tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See CLI examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from dateutil.tz import tzlocal 41from time import sleep 42 43import re 44import json 45import requests 46import traceback as tb 47from typing import Union 48 49from multiprocessing import cpu_count, Lock 50from multiprocessing.pool import ThreadPool 51import pandas as pd 52 53from mako.template import Template # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 54from Templates import * # Some html-templates used by reporting methods in TKSBrokerAPI module 55from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 56from TradeRoutines import * # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module 57 58from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator) 59from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 60 61import UniLogger as uLog # Logger for TKSBrokerAPI 62 63 64# --- Common technical parameters: 65 66PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 67uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 68uLogger.level = 10 # debug level by default for TKSBrokerAPI module 69uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 70 71__version__ = "1.6" # The "major.minor" version setup here, but build number define at the build-server only 72 73CPU_COUNT = cpu_count() # host's real CPU count 74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 75 76 77class TinkoffBrokerServer: 78 """ 79 This class implements methods to work with Tinkoff broker server. 80 81 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 82 83 About `token`: https://tinkoff.github.io/investAPI/token/ 84 """ 85 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 86 """ 87 Main class init. 88 89 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 90 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 91 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 92 :param useCache: use default cache file with raw data to use instead of `iList`. 93 True by default. Cache is auto-update if new day has come. 94 If you don't want to use cache and always updates raw data then set `useCache=False`. 95 :param defaultCache: path to default cache file. `dump.json` by default. 96 """ 97 if token is None or not token: 98 try: 99 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 100 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 101 102 except KeyError: 103 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 104 raise Exception("Token required") 105 106 else: 107 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 108 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 109 110 if accountId is None or not accountId: 111 try: 112 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 113 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 114 115 except KeyError: 116 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 117 118 else: 119 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 120 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 121 122 self.version = __version__ # duplicate here used TKSBrokerAPI main version 123 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 124 125 Latest version: https://pypi.org/project/tksbrokerapi/ 126 """ 127 128 self._tag = "" 129 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 130 131 self.__lock = Lock() # initialize multiprocessing mutex lock 132 133 self.aliases = TKS_TICKER_ALIASES 134 """Some aliases instead official tickers. 135 136 See also: `TKSEnums.TKS_TICKER_ALIASES` 137 """ 138 139 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 140 141 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 142 143 self._ticker = "" 144 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 145 146 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 147 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 148 149 See also: `SearchByTicker()`, `SearchInstruments()`. 150 """ 151 152 self._figi = "" 153 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 154 155 See also: `SearchByFIGI()`, `SearchInstruments()`. 156 """ 157 158 self.depth = 1 159 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 160 161 See also: `GetCurrentPrices()`. 162 """ 163 164 self.server = r"https://invest-public-api.tinkoff.ru/rest" 165 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 166 167 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 168 """ 169 170 uLogger.debug("Broker API server: {}".format(self.server)) 171 172 self.timeout = 15 173 """Server operations timeout in seconds. Default: `15`. 174 175 See also: `SendAPIRequest()`. 176 """ 177 178 self.headers = { 179 "Content-Type": "application/json", 180 "accept": "application/json", 181 "Authorization": "Bearer {}".format(self.token), 182 "x-app-name": "Tim55667757.TKSBrokerAPI", 183 } 184 """ 185 Headers which send in every request to broker server. Please, do not change it! 186 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 187 188 See also: `SendAPIRequest()`. 189 """ 190 191 self.body = None 192 """Request body which send to broker server. Default: `None`. 193 194 See also: `SendAPIRequest()`. 195 """ 196 197 self.moreDebug = False 198 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 199 200 self.useHTMLReports = False 201 """ 202 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 203 204 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 205 """ 206 207 self.historyFile = None 208 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 209 210 See also: `History()`. 211 """ 212 213 self.htmlHistoryFile = "index.html" 214 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 215 216 See also: `ShowHistoryChart()`. 217 """ 218 219 self.instrumentsFile = "instruments.md" 220 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 221 222 See also: `ShowInstrumentsInfo()`. 223 """ 224 225 self.searchResultsFile = "search-results.md" 226 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 227 228 See also: `SearchInstruments()`. 229 """ 230 231 self.pricesFile = "prices.md" 232 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 233 234 See also: `GetListOfPrices()`. 235 """ 236 237 self.infoFile = "info.md" 238 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 239 240 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 241 """ 242 243 self.bondsXLSXFile = "ext-bonds.xlsx" 244 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 245 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 246 247 See also: `ExtendBondsData()`. 248 """ 249 250 self.calendarFile = "calendar.md" 251 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 252 253 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 254 255 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 256 """ 257 258 self.overviewFile = "overview.md" 259 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 260 261 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 262 """ 263 264 self.overviewDigestFile = "overview-digest.md" 265 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 266 267 See also: `Overview()` with parameter `details="digest"`. 268 """ 269 270 self.overviewPositionsFile = "overview-positions.md" 271 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 272 273 See also: `Overview()` with parameter `details="positions"`. 274 """ 275 276 self.overviewOrdersFile = "overview-orders.md" 277 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 278 279 See also: `Overview()` with parameter `details="orders"`. 280 """ 281 282 self.overviewAnalyticsFile = "overview-analytics.md" 283 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 284 285 See also: `Overview()` with parameter `details="analytics"`. 286 """ 287 288 self.overviewBondsCalendarFile = "overview-calendar.md" 289 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 290 291 See also: `Overview()` with parameter `details="calendar"`. 292 """ 293 294 self.reportFile = "deals.md" 295 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 296 297 See also: `Deals()`. 298 """ 299 300 self.withdrawalLimitsFile = "limits.md" 301 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 302 303 See also: `OverviewLimits()` and `RequestLimits()`. 304 """ 305 306 self.userInfoFile = "user-info.md" 307 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 308 309 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 310 """ 311 312 self.userAccountsFile = "accounts.md" 313 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 314 315 See also: `OverviewAccounts()`, `RequestAccounts()`. 316 """ 317 318 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 319 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 320 321 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 322 323 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 324 """ 325 326 self.iList = None # init iList for raw instruments data 327 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 328 329 See also: `Listing()`, `DumpInstruments()`. 330 """ 331 332 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 333 if useCache: 334 if os.path.exists(self.iListDumpFile): 335 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 336 curTime = datetime.now(tzutc()) 337 338 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 339 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 340 341 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 342 343 else: 344 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 345 346 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 347 os.path.abspath(self.iListDumpFile), 348 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 349 )) 350 351 else: 352 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 353 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 354 355 else: 356 self.iList = self.Listing() # request new raw instruments data from broker server 357 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 358 359 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 360 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 361 362 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 363 """ 364 365 @property 366 def tag(self) -> str: 367 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 368 return self._tag 369 370 @tag.setter 371 def tag(self, value): 372 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 373 self._tag = str(value) 374 375 if self._tag: 376 for handler in uLogger.handlers: 377 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 378 379 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 380 381 else: 382 for handler in uLogger.handlers: 383 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 384 385 uLogger.debug("Default logger format is used") 386 387 @property 388 def ticker(self) -> str: 389 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 390 391 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 392 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 393 394 See also: `SearchByTicker()`, `SearchInstruments()`. 395 """ 396 return self._ticker 397 398 @ticker.setter 399 def ticker(self, value): 400 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 401 402 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 403 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 404 405 See also: `SearchByTicker()`, `SearchInstruments()`. 406 """ 407 self._ticker = str(value).upper() # Tickers may be upper case only 408 409 @property 410 def figi(self) -> str: 411 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 412 413 See also: `SearchByFIGI()`, `SearchInstruments()`. 414 """ 415 return self._figi 416 417 @figi.setter 418 def figi(self, value): 419 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 420 421 See also: `SearchByFIGI()`, `SearchInstruments()`. 422 """ 423 self._figi = str(value).upper() # FIGI may be upper case only 424 425 def _ParseJSON(self, rawData="{}") -> dict: 426 """ 427 Parse JSON from response string. 428 429 :param rawData: this is a string with JSON-formatted text. 430 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 431 """ 432 try: 433 responseJSON = json.loads(rawData) if rawData else {} 434 435 if self.moreDebug: 436 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 437 438 return responseJSON 439 440 except Exception as e: 441 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 442 443 return {} 444 445 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 446 """ 447 Send GET or POST request to broker server and receive JSON object. 448 449 self.header: must be defining with dictionary of headers. 450 self.body: if define then used as request body. None by default. 451 self.timeout: global request timeout, 15 seconds by default. 452 :param url: url with REST request. 453 :param reqType: send "GET" or "POST" request. "GET" by default. 454 :param retry: how many times retry after first request if an 5xx server errors occurred. 455 :param pause: sleep time in seconds between retries. 456 :return: response JSON (dictionary) from broker. 457 """ 458 if reqType.upper() not in ("GET", "POST"): 459 uLogger.error("You can define request type: `GET` or `POST`!") 460 raise Exception("Incorrect value") 461 462 if self.moreDebug: 463 uLogger.debug("Request parameters:") 464 uLogger.debug(" - REST API URL: {}".format(url)) 465 uLogger.debug(" - request type: {}".format(reqType)) 466 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 467 uLogger.debug(" - body:\n{}".format(self.body)) 468 469 # fast hack to avoid all operations with some tickers/FIGI 470 responseJSON = {} 471 oK = True 472 for item in self.exclude: 473 if item in url: 474 if self.moreDebug: 475 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 476 477 oK = False 478 break 479 480 if oK: 481 with self.__lock: # acquire the mutex lock 482 counter = 0 483 response = None 484 errMsg = "" 485 486 while not response and counter <= retry: 487 if reqType == "GET": 488 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 489 490 if reqType == "POST": 491 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 492 493 if self.moreDebug: 494 uLogger.debug("Response:") 495 uLogger.debug(" - status code: {}".format(response.status_code)) 496 uLogger.debug(" - reason: {}".format(response.reason)) 497 uLogger.debug(" - body length: {}".format(len(response.text))) 498 uLogger.debug(" - headers:\n{}".format(response.headers)) 499 500 # Server returns some headers: 501 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 502 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 503 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 504 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 505 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 506 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 507 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 508 sleep(rateLimitWait) 509 510 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 511 if 400 <= response.status_code < 500: 512 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 513 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 514 515 if "code" in response.text and "message" in response.text: 516 msgDict = self._ParseJSON(rawData=response.text) 517 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 518 519 counter = retry + 1 # do not retry for 4xx errors 520 521 if 500 <= response.status_code < 600: 522 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 523 uLogger.debug(" - not oK, {}".format(errMsg)) 524 525 if "code" in response.text and "message" in response.text: 526 errMsgDict = self._ParseJSON(rawData=response.text) 527 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 528 529 counter += 1 530 531 if counter <= retry: 532 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 533 sleep(pause) 534 535 responseJSON = self._ParseJSON(rawData=response.text) 536 537 if errMsg: 538 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 539 uLogger.error(" - not oK, {}".format(errMsg)) 540 541 return responseJSON 542 543 def _IUpdater(self, iType: str) -> tuple: 544 """ 545 Request instrument by type from server. See available API methods for instruments: 546 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 547 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 548 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 549 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 550 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 551 552 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 553 :return: tuple with iType name and list of available instruments of current type for defined user token. 554 """ 555 result = [] 556 557 if iType in TKS_INSTRUMENTS: 558 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 559 560 # all instruments have the same body in API v2 requests: 561 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 562 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 563 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 564 565 return iType, result 566 567 def _IWrapper(self, kwargs): 568 """ 569 Wrapper runs instrument's update method `_IUpdater()`. 570 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 571 """ 572 return self._IUpdater(**kwargs) 573 574 def Listing(self) -> dict: 575 """ 576 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 577 578 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 579 """ 580 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 581 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 582 583 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 584 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 585 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 586 587 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 588 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 589 poolUpdater.close() # close the thread pool 590 poolUpdater.join() # wait a moment until all data returns from threads 591 592 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 593 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 594 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 595 596 # calculate minimum price increment (step) for all instruments and set up instrument's type: 597 for iType in iList.keys(): 598 for ticker in iList[iType]: 599 iList[iType][ticker]["type"] = iType 600 601 if "minPriceIncrement" in iList[iType][ticker].keys(): 602 iList[iType][ticker]["step"] = NanoToFloat( 603 iList[iType][ticker]["minPriceIncrement"]["units"], 604 iList[iType][ticker]["minPriceIncrement"]["nano"], 605 ) 606 607 else: 608 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 609 610 return iList 611 612 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 613 """ 614 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 615 616 See also: `DumpInstruments()`, `Listing()`. 617 618 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 619 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 620 """ 621 if self.iListDumpFile is None or not self.iListDumpFile: 622 uLogger.error("Output name of dump file must be defined!") 623 raise Exception("Filename required") 624 625 if not self.iList or forceUpdate: 626 self.iList = self.Listing() 627 628 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 629 630 # Save as XLSX with separated sheets for every type of instruments: 631 with pd.ExcelWriter( 632 path=xlsxDumpFile, 633 date_format=TKS_DATE_FORMAT, 634 datetime_format=TKS_DATE_TIME_FORMAT, 635 mode="w", 636 ) as writer: 637 for iType in TKS_INSTRUMENTS: 638 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 639 df = df[sorted(df)] # sorted by column names 640 df = df.applymap( 641 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 642 na_action="ignore", 643 ) # converting numbers from nano-type to float in every cell 644 df.to_excel( 645 writer, 646 sheet_name=iType, 647 encoding="UTF-8", 648 freeze_panes=(1, 1), 649 ) # saving as XLSX-file with freeze first row and column as headers 650 651 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 652 653 def DumpInstruments(self, forceUpdate: bool = True) -> str: 654 """ 655 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 656 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 657 658 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 659 660 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 661 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 662 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 663 """ 664 if self.iListDumpFile is None or not self.iListDumpFile: 665 uLogger.error("Output name of dump file must be defined!") 666 raise Exception("Filename required") 667 668 if not self.iList or forceUpdate: 669 self.iList = self.Listing() 670 671 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 672 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 673 fH.write(jsonDump) 674 675 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 676 677 return jsonDump 678 679 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 680 """ 681 Show information about one instrument defined by json data and prints it in Markdown format. 682 683 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 684 685 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 686 :param show: if `True` then also printing information about instrument and its current price. 687 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 688 :return: multilines text in Markdown format with information about one instrument. 689 """ 690 splitLine = "| | |\n" 691 infoText = "" 692 693 if iJSON is not None and iJSON and isinstance(iJSON, dict): 694 info = [ 695 "# Main information\n\n", 696 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 697 "| Parameters | Values |\n", 698 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 699 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 700 "| Full name: | {:<54} |\n".format(iJSON["name"]), 701 ] 702 703 if "sector" in iJSON.keys() and iJSON["sector"]: 704 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 705 706 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 707 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 708 709 info.extend([ 710 splitLine, 711 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 712 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 713 ]) 714 715 if "isin" in iJSON.keys() and iJSON["isin"]: 716 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 717 718 if "classCode" in iJSON.keys(): 719 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 720 721 info.extend([ 722 splitLine, 723 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 724 splitLine, 725 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 726 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 727 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 728 ]) 729 730 if iJSON["figi"]: 731 self._figi = iJSON["figi"] 732 iJSON = iJSON | self.RequestTradingStatus() 733 734 info.extend([ 735 splitLine, 736 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 737 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 738 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 739 ]) 740 741 info.append(splitLine) 742 743 if "type" in iJSON.keys() and iJSON["type"]: 744 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 745 746 if "shareType" in iJSON.keys() and iJSON["shareType"]: 747 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 748 749 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 750 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 751 752 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 753 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 754 755 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 756 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 757 758 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 759 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 760 761 if "focusType" in iJSON.keys() and iJSON["focusType"]: 762 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 763 764 if "assetType" in iJSON.keys() and iJSON["assetType"]: 765 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 766 767 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 768 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 769 770 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 771 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 772 773 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 774 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 775 776 if "currency" in iJSON.keys(): 777 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 778 779 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 780 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 781 782 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 783 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 784 785 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 786 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 787 788 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 789 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 790 791 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 792 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 793 794 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 795 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 796 797 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 798 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 799 800 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 801 info.append("| Perpetual bond: | Yes |\n") 802 803 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 804 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 805 806 iExt = None 807 if iJSON["type"] == "Bonds": 808 info.extend([ 809 splitLine, 810 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 811 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 812 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 813 iJSON["nominal"]["currency"], 814 )), 815 ]) 816 817 if "floatingCouponFlag" in iJSON.keys(): 818 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 819 820 if "amortizationFlag" in iJSON.keys(): 821 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 822 823 info.append(splitLine) 824 825 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 826 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 827 828 if iJSON["figi"]: 829 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 830 831 info.extend([ 832 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 833 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 834 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 835 ]) 836 837 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 838 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 839 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 840 iJSON["aciValue"]["currency"] 841 ))) 842 843 if "currentPrice" in iJSON.keys(): 844 info.append(splitLine) 845 846 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 847 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 848 849 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 850 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 851 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 852 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 853 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 854 855 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 856 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 857 858 info.extend([ 859 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 860 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 861 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 862 )), 863 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 864 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 865 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 866 )), 867 "| Changes between last deal price and last close | {:<54} |\n".format( 868 "{:.2f}%{}".format( 869 iJSON["currentPrice"]["changes"], 870 " ({}{:.2f} {})".format( 871 "+" if bondChangesDelta > 0 else "", 872 bondChangesDelta, 873 aciCurrency 874 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 875 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 876 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 877 currency 878 ), 879 ) 880 ), 881 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 882 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 883 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 884 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 885 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 886 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 887 )), 888 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 889 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 890 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 891 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 892 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 893 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 894 )), 895 ]) 896 897 if "lot" in iJSON.keys(): 898 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 899 900 if "step" in iJSON.keys() and iJSON["step"] != 0: 901 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 902 903 # Add bond payment calendar: 904 if iJSON["type"] == "Bonds": 905 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 906 info.extend(["\n#", strCalendar]) 907 908 infoText += "".join(info) 909 910 if show and not onlyFiles: 911 uLogger.info("{}".format(infoText)) 912 913 if self.infoFile is not None and (show or onlyFiles): 914 with open(self.infoFile, "w", encoding="UTF-8") as fH: 915 fH.write(infoText) 916 917 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 918 919 if self.useHTMLReports: 920 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 921 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 922 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 923 924 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 925 926 return infoText 927 928 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 929 """ 930 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 931 932 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 933 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 934 :return: JSON formatted data with information about instrument. 935 """ 936 tickerJSON = {} 937 if self.moreDebug: 938 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 939 940 if not self._ticker: 941 uLogger.warning("self._ticker variable is not be empty!") 942 943 else: 944 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 945 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 946 raise Exception("Instrument not allowed") 947 948 if not self.iList: 949 self.iList = self.Listing() 950 951 if self._ticker in self.iList["Shares"].keys(): 952 tickerJSON = self.iList["Shares"][self._ticker] 953 if self.moreDebug: 954 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 955 956 elif self._ticker in self.iList["Currencies"].keys(): 957 tickerJSON = self.iList["Currencies"][self._ticker] 958 if self.moreDebug: 959 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 960 961 elif self._ticker in self.iList["Bonds"].keys(): 962 tickerJSON = self.iList["Bonds"][self._ticker] 963 if self.moreDebug: 964 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 965 966 elif self._ticker in self.iList["Etfs"].keys(): 967 tickerJSON = self.iList["Etfs"][self._ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 970 971 elif self._ticker in self.iList["Futures"].keys(): 972 tickerJSON = self.iList["Futures"][self._ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 975 976 if tickerJSON: 977 self._figi = tickerJSON["figi"] 978 979 if requestPrice: 980 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 981 982 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 983 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 984 985 else: 986 tickerJSON["currentPrice"]["changes"] = 0 987 988 if show: 989 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 990 991 else: 992 if show: 993 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 994 995 return tickerJSON 996 997 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 998 """ 999 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1000 1001 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1002 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1003 :return: JSON formatted data with information about instrument. 1004 """ 1005 figiJSON = {} 1006 if self.moreDebug: 1007 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1008 1009 if not self._figi: 1010 uLogger.warning("self._figi variable is not be empty!") 1011 1012 else: 1013 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1014 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1015 raise Exception("Instrument not allowed") 1016 1017 if not self.iList: 1018 self.iList = self.Listing() 1019 1020 for item in self.iList["Shares"].keys(): 1021 if self._figi == self.iList["Shares"][item]["figi"]: 1022 figiJSON = self.iList["Shares"][item] 1023 1024 if self.moreDebug: 1025 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1026 1027 break 1028 1029 if not figiJSON: 1030 for item in self.iList["Currencies"].keys(): 1031 if self._figi == self.iList["Currencies"][item]["figi"]: 1032 figiJSON = self.iList["Currencies"][item] 1033 1034 if self.moreDebug: 1035 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1036 1037 break 1038 1039 if not figiJSON: 1040 for item in self.iList["Bonds"].keys(): 1041 if self._figi == self.iList["Bonds"][item]["figi"]: 1042 figiJSON = self.iList["Bonds"][item] 1043 1044 if self.moreDebug: 1045 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1046 1047 break 1048 1049 if not figiJSON: 1050 for item in self.iList["Etfs"].keys(): 1051 if self._figi == self.iList["Etfs"][item]["figi"]: 1052 figiJSON = self.iList["Etfs"][item] 1053 1054 if self.moreDebug: 1055 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1056 1057 break 1058 1059 if not figiJSON: 1060 for item in self.iList["Futures"].keys(): 1061 if self._figi == self.iList["Futures"][item]["figi"]: 1062 figiJSON = self.iList["Futures"][item] 1063 1064 if self.moreDebug: 1065 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1066 1067 break 1068 1069 if figiJSON: 1070 self._figi = figiJSON["figi"] 1071 self._ticker = figiJSON["ticker"] 1072 1073 if requestPrice: 1074 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1075 1076 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1077 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1078 1079 else: 1080 figiJSON["currentPrice"]["changes"] = 0 1081 1082 if show: 1083 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1084 1085 else: 1086 if show: 1087 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1088 1089 return figiJSON 1090 1091 def GetCurrentPrices(self, show: bool = True) -> dict: 1092 """ 1093 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1094 `{"buy": [{"price": 1243.8, "quantity": 193}, 1095 {"price": 1244.0, "quantity": 168}, 1096 {"price": 1244.8, "quantity": 5}, 1097 {"price": 1245.0, "quantity": 61}, 1098 {"price": 1245.4, "quantity": 60}], 1099 "sell": [{"price": 1243.6, "quantity": 8}, 1100 {"price": 1242.6, "quantity": 10}, 1101 {"price": 1242.4, "quantity": 18}, 1102 {"price": 1242.2, "quantity": 50}, 1103 {"price": 1242.0, "quantity": 113}], 1104 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1105 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1106 - sell: list of dicts with Buyers prices, 1107 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1108 - quantity: volume value by current price in lots, 1109 - limitUp: current trade session limit price, maximum, 1110 - limitDown: current trade session limit price, minimum, 1111 - lastPrice: last deal price of the instrument, 1112 - closePrice: previous trade session close price of the instrument. 1113 1114 See also: `SearchByTicker()` and `SearchByFIGI()`. 1115 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1116 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1117 1118 :param show: if `True` then print DOM to log and console. 1119 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1120 If an error occurred then returns an empty record: 1121 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1122 """ 1123 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1124 1125 if self.depth < 1: 1126 uLogger.error("Depth of Market (DOM) must be >=1!") 1127 raise Exception("Incorrect value") 1128 1129 if not (self._ticker or self._figi): 1130 uLogger.error("self._ticker or self._figi variables must be defined!") 1131 raise Exception("Ticker or FIGI required") 1132 1133 if self._ticker and not self._figi: 1134 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1135 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1136 1137 if not self._ticker and self._figi: 1138 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1139 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1140 1141 if not self._figi: 1142 uLogger.error("FIGI is not defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 else: 1146 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1147 1148 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1149 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1150 self.body = str({"figi": self._figi, "depth": self.depth}) 1151 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1152 1153 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1154 # list of dicts with sellers orders: 1155 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1156 1157 # list of dicts with buyers orders: 1158 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1159 1160 # max price of instrument at this time: 1161 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1162 1163 # min price of instrument at this time: 1164 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1165 1166 # last price of deal with instrument: 1167 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1168 1169 # last close price of instrument: 1170 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1171 1172 else: 1173 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1174 uLogger.debug("Server response: {}".format(pricesResponse)) 1175 1176 if show: 1177 if prices["buy"] or prices["sell"]: 1178 info = [ 1179 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1180 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1181 self._ticker, 1182 self._figi, 1183 self.depth, 1184 ), 1185 "-" * 60, "\n", 1186 " Orders of Buyers | Orders of Sellers\n", 1187 "-" * 60, "\n", 1188 " Sell prices (volumes) | Buy prices (volumes)\n", 1189 "-" * 60, "\n", 1190 ] 1191 1192 if not prices["buy"]: 1193 info.append(" | No orders!\n") 1194 sumBuy = 0 1195 1196 else: 1197 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1198 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1199 for item in maxMinSorted: 1200 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1201 1202 if not prices["sell"]: 1203 info.append("No orders! |\n") 1204 sumSell = 0 1205 1206 else: 1207 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1208 for item in prices["sell"]: 1209 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1210 1211 info.extend([ 1212 "-" * 60, "\n", 1213 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1214 "-" * 60, "\n", 1215 ]) 1216 1217 infoText = "".join(info) 1218 1219 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1220 1221 else: 1222 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1223 1224 return prices 1225 1226 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1227 """ 1228 This method get and show information about all available broker instruments for current user account. 1229 If `instrumentsFile` string is not empty then also save information to this file. 1230 1231 :param show: if `True` then print results to console, if `False` — print only to file. 1232 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1233 :return: multi-lines string with all available broker instruments. 1234 """ 1235 if not self.iList: 1236 self.iList = self.Listing() 1237 1238 info = [ 1239 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1240 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1241 ] 1242 1243 # add instruments count by type: 1244 for iType in self.iList.keys(): 1245 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1246 1247 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1248 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1249 1250 # generating info tables with all instruments by type: 1251 for iType in self.iList.keys(): 1252 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1253 1254 for instrument in self.iList[iType].keys(): 1255 iName = self.iList[iType][instrument]["name"] # instrument's name 1256 if len(iName) > 57: 1257 iName = "{}...".format(iName[:54]) # right trim for a long string 1258 1259 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1260 self.iList[iType][instrument]["ticker"], 1261 iName, 1262 self.iList[iType][instrument]["figi"], 1263 self.iList[iType][instrument]["currency"], 1264 self.iList[iType][instrument]["lot"], 1265 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1266 )) 1267 1268 infoText = "".join(info) 1269 1270 if show and not onlyFiles: 1271 uLogger.info(infoText) 1272 1273 if self.instrumentsFile and (show or onlyFiles): 1274 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1275 fH.write(infoText) 1276 1277 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1278 1279 if self.useHTMLReports: 1280 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1281 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1282 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1283 1284 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1285 1286 return infoText 1287 1288 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1289 """ 1290 This method search and show information about instruments by part of its ticker, FIGI or name. 1291 If `searchResultsFile` string is not empty then also save information to this file. 1292 1293 :param pattern: string with part of ticker, FIGI or instrument's name. 1294 :param show: if `True` then print results to console, if `False` — return list of result only. 1295 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1296 :return: list of dictionaries with all found instruments. 1297 """ 1298 if not self.iList: 1299 self.iList = self.Listing() 1300 1301 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1302 compiledPattern = re.compile(pattern, re.IGNORECASE) 1303 1304 for iType in self.iList: 1305 for instrument in self.iList[iType].values(): 1306 searchResult = compiledPattern.search(" ".join( 1307 [instrument["ticker"], instrument["figi"], instrument["name"]] 1308 )) 1309 1310 if searchResult: 1311 searchResults[iType][instrument["ticker"]] = instrument 1312 1313 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1314 info = [ 1315 "# Search results\n\n", 1316 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1317 "* **Search pattern:** [{}]\n".format(pattern), 1318 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1319 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1320 ] 1321 infoShort = info[:] 1322 1323 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1324 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1325 skippedLine = "| ... | ... | ... | ... |\n" 1326 1327 if resultsLen == 0: 1328 info.append("\nNo results\n") 1329 infoShort.append("\nNo results\n") 1330 uLogger.warning("No results. Try changing your search pattern.") 1331 1332 else: 1333 for iType in searchResults: 1334 iTypeValuesCount = len(searchResults[iType].values()) 1335 if iTypeValuesCount > 0: 1336 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1337 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1338 1339 for instrument in searchResults[iType].values(): 1340 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1341 instrument["type"], 1342 instrument["ticker"], 1343 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1344 instrument["figi"], 1345 )) 1346 1347 if iTypeValuesCount <= 5: 1348 infoShort.extend(info[-iTypeValuesCount:]) 1349 1350 else: 1351 infoShort.extend(info[-5:]) 1352 infoShort.append(skippedLine) 1353 1354 infoText = "".join(info) 1355 infoTextShort = "".join(infoShort) 1356 1357 if show and not onlyFiles: 1358 uLogger.info(infoTextShort) 1359 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1360 1361 if self.searchResultsFile and (show or onlyFiles): 1362 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1363 fH.write(infoText) 1364 1365 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1366 1367 if self.useHTMLReports: 1368 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1369 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1370 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1371 1372 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1373 1374 return searchResults 1375 1376 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1377 """ 1378 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1379 1380 :param instruments: list of strings with tickers or FIGIs. 1381 :return: list with unique instrument FIGIs only. 1382 """ 1383 requestedInstruments = [] 1384 for iName in instruments: 1385 if iName not in self.aliases.keys(): 1386 if iName not in requestedInstruments: 1387 requestedInstruments.append(iName) 1388 1389 else: 1390 if iName not in requestedInstruments: 1391 if self.aliases[iName] not in requestedInstruments: 1392 requestedInstruments.append(self.aliases[iName]) 1393 1394 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1395 1396 onlyUniqueFIGIs = [] 1397 for iName in requestedInstruments: 1398 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1399 continue 1400 1401 self._ticker = iName 1402 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1403 1404 if not iData: 1405 self._ticker = "" 1406 self._figi = iName 1407 1408 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1409 1410 if not iData: 1411 self._figi = "" 1412 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1413 1414 if iData and iData["figi"] not in onlyUniqueFIGIs: 1415 onlyUniqueFIGIs.append(iData["figi"]) 1416 1417 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1418 1419 return onlyUniqueFIGIs 1420 1421 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1422 """ 1423 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1424 1425 See limits: https://tinkoff.github.io/investAPI/limits/ 1426 1427 If `pricesFile` string is not empty then also save information to this file. 1428 1429 :param instruments: list of strings with tickers or FIGIs. 1430 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1431 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1432 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1433 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1434 """ 1435 if instruments is None or not instruments: 1436 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1437 raise Exception("Ticker or FIGI required") 1438 1439 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1440 1441 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1442 1443 iList = [] # trying to get info and current prices about all unique instruments: 1444 for self._figi in onlyUniqueFIGIs: 1445 iData = self.SearchByFIGI(requestPrice=True, show=False) 1446 iList.append(iData) 1447 1448 self.ShowListOfPrices(iList, show, onlyFiles) 1449 1450 return iList 1451 1452 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1453 """ 1454 Show table contains current prices of given instruments. 1455 1456 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1457 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1458 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1459 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1460 :return: multilines text in Markdown format as a table contains current prices. 1461 """ 1462 infoText = "" 1463 1464 if show or self.pricesFile or onlyFiles: 1465 info = [ 1466 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1467 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1468 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1469 ] 1470 1471 for item in iList: 1472 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1473 item["ticker"], 1474 item["figi"], 1475 item["type"], 1476 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1477 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1478 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1479 "{} / {}".format( 1480 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1481 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1482 ), 1483 "{} / {}".format( 1484 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1485 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1486 ), 1487 item["currency"], 1488 )) 1489 1490 infoText = "".join(info) 1491 1492 if show and not onlyFiles: 1493 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1494 1495 if self.pricesFile and (show or onlyFiles): 1496 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1497 fH.write(infoText) 1498 1499 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1500 1501 if self.useHTMLReports: 1502 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1503 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1504 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1505 1506 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1507 1508 return infoText 1509 1510 def RequestTradingStatus(self) -> dict: 1511 """ 1512 Requesting trading status for the instrument defined by `figi` variable. 1513 1514 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1515 1516 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1517 1518 :return: dictionary with trading status attributes. Response example: 1519 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1520 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1521 """ 1522 if self._figi is None or not self._figi: 1523 uLogger.error("Variable `figi` must be defined for using this method!") 1524 raise Exception("FIGI required") 1525 1526 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1527 1528 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1529 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1530 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1531 1532 if self.moreDebug: 1533 uLogger.debug("Records about current trading status successfully received") 1534 1535 return tradingStatus 1536 1537 def RequestPortfolio(self) -> dict: 1538 """ 1539 Requesting actual user's portfolio for current `accountId`. 1540 1541 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1542 1543 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1544 1545 :return: dictionary with user's portfolio. 1546 """ 1547 if self.accountId is None or not self.accountId: 1548 uLogger.error("Variable `accountId` must be defined for using this method!") 1549 raise Exception("Account ID required") 1550 1551 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1552 1553 self.body = str({"accountId": self.accountId}) 1554 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1555 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1556 1557 if self.moreDebug: 1558 uLogger.debug("Records about user's portfolio successfully received") 1559 1560 return rawPortfolio 1561 1562 def RequestPositions(self) -> dict: 1563 """ 1564 Requesting open positions by currencies and instruments for current `accountId`. 1565 1566 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1567 1568 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1569 1570 :return: dictionary with open positions by instruments. 1571 """ 1572 if self.accountId is None or not self.accountId: 1573 uLogger.error("Variable `accountId` must be defined for using this method!") 1574 raise Exception("Account ID required") 1575 1576 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1577 1578 self.body = str({"accountId": self.accountId}) 1579 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1580 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1581 1582 if self.moreDebug: 1583 uLogger.debug("Records about current open positions successfully received") 1584 1585 return rawPositions 1586 1587 def RequestPendingOrders(self) -> list: 1588 """ 1589 Requesting current actual pending limit orders for current `accountId`. 1590 1591 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1592 1593 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1594 1595 :return: list of dictionaries with pending limit orders. 1596 """ 1597 if self.accountId is None or not self.accountId: 1598 uLogger.error("Variable `accountId` must be defined for using this method!") 1599 raise Exception("Account ID required") 1600 1601 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1602 1603 self.body = str({"accountId": self.accountId}) 1604 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1605 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1606 1607 if "orders" in rawResponse.keys(): 1608 rawOrders = rawResponse["orders"] 1609 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1610 1611 else: 1612 rawOrders = [] 1613 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1614 1615 return rawOrders 1616 1617 def RequestStopOrders(self) -> list: 1618 """ 1619 Requesting current actual stop orders for current `accountId`. 1620 1621 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1622 1623 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1624 1625 :return: list of dictionaries with stop orders. 1626 """ 1627 if self.accountId is None or not self.accountId: 1628 uLogger.error("Variable `accountId` must be defined for using this method!") 1629 raise Exception("Account ID required") 1630 1631 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1632 1633 self.body = str({"accountId": self.accountId}) 1634 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1635 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1636 1637 if "stopOrders" in rawResponse.keys(): 1638 rawStopOrders = rawResponse["stopOrders"] 1639 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1640 1641 else: 1642 rawStopOrders = [] 1643 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1644 1645 return rawStopOrders 1646 1647 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1648 """ 1649 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1650 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1651 and `overviewBondsCalendarFile` are defined then also save information to file. 1652 1653 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1654 many requests about the state of the portfolio, and then, based on the received data, a large number 1655 of calculation and statistics are collected. 1656 1657 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1658 :param details: how detailed should the information be? 1659 - `full` — shows full available information about portfolio status (by default), 1660 - `positions` — shows only open positions, 1661 - `orders` — shows only sections of open limits and stop orders. 1662 - `digest` — show a short digest of the portfolio status, 1663 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1664 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1665 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1666 :return: dictionary with client's raw portfolio and some statistics. 1667 """ 1668 if self.accountId is None or not self.accountId: 1669 uLogger.error("Variable `accountId` must be defined for using this method!") 1670 raise Exception("Account ID required") 1671 1672 view = { 1673 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1674 "headers": {}, # list of dictionaries, response headers without "positions" section 1675 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1676 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1677 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1678 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1679 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1680 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1681 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1682 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1683 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1684 }, 1685 "stat": { # --- some statistics calculated using "raw" sections: 1686 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1687 "availableRUB": 0., # available rubles (without other currencies) 1688 "blockedRUB": 0., # blocked sum in Russian Rouble 1689 "totalChangesRUB": 0., # changes for all open trades in RUB 1690 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1691 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1692 "sharesCostRUB": 0., # costs of all shares in RUB 1693 "bondsCostRUB": 0., # costs of all bonds in RUB 1694 "etfsCostRUB": 0., # costs of all etfs in RUB 1695 "futuresCostRUB": 0., # costs of all futures in RUB 1696 "Currencies": [], # list of dictionaries of all currencies statistics 1697 "Shares": [], # list of dictionaries of all shares statistics 1698 "Bonds": [], # list of dictionaries of all bonds statistics 1699 "Etfs": [], # list of dictionaries of all etfs statistics 1700 "Futures": [], # list of dictionaries of all futures statistics 1701 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1702 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1703 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1704 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1705 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1706 }, 1707 "analytics": { # --- some analytics of portfolio: 1708 "distrByAssets": {}, # portfolio distribution by assets 1709 "distrByCompanies": {}, # portfolio distribution by companies 1710 "distrBySectors": {}, # portfolio distribution by sectors 1711 "distrByCurrencies": {}, # portfolio distribution by currencies 1712 "distrByCountries": {}, # portfolio distribution by countries 1713 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1714 } 1715 } 1716 1717 details = details.lower() 1718 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1719 if details not in availableDetails: 1720 details = "full" 1721 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1722 1723 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1724 1725 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1726 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1727 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1728 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1729 1730 # save response headers without "positions" section: 1731 for key in portfolioResponse.keys(): 1732 if key != "positions": 1733 view["raw"]["headers"][key] = portfolioResponse[key] 1734 1735 else: 1736 continue 1737 1738 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1739 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1740 for item in portfolioResponse["positions"]: 1741 if item["instrumentType"] == "currency": 1742 self._figi = item["figi"] 1743 if not self._figi and item["ticker"]: 1744 self._ticker = item["ticker"] 1745 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1746 1747 curr = self.SearchByFIGI(requestPrice=False) 1748 1749 # current price of currency in RUB: 1750 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1751 "name": curr["name"], 1752 "currentPrice": NanoToFloat( 1753 item["currentPrice"]["units"], 1754 item["currentPrice"]["nano"] 1755 ), 1756 } 1757 1758 view["raw"]["Currencies"].append(item) 1759 1760 elif item["instrumentType"] == "share": 1761 view["raw"]["Shares"].append(item) 1762 1763 elif item["instrumentType"] == "bond": 1764 view["raw"]["Bonds"].append(item) 1765 1766 elif item["instrumentType"] == "etf": 1767 view["raw"]["Etfs"].append(item) 1768 1769 elif item["instrumentType"] == "futures": 1770 view["raw"]["Futures"].append(item) 1771 1772 else: 1773 continue 1774 1775 # how many volume of currencies (by ISO currency name) are blocked: 1776 for item in view["raw"]["positions"]["blocked"]: 1777 blocked = NanoToFloat(item["units"], item["nano"]) 1778 if blocked > 0: 1779 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1780 1781 # how many volume of instruments (by FIGI) are blocked: 1782 for item in view["raw"]["positions"]["securities"]: 1783 blocked = int(item["blocked"]) 1784 if blocked > 0: 1785 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1786 1787 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1788 1789 if "rub" in allBlocked.keys(): 1790 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1791 1792 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1793 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1794 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1795 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1796 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1797 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1798 view["stat"]["portfolioCostRUB"] = sum([ 1799 view["stat"]["allCurrenciesCostRUB"], 1800 view["stat"]["sharesCostRUB"], 1801 view["stat"]["bondsCostRUB"], 1802 view["stat"]["etfsCostRUB"], 1803 view["stat"]["futuresCostRUB"], 1804 ]) 1805 1806 # --- calculating some portfolio statistics: 1807 byComp = {} # distribution by companies 1808 bySect = {} # distribution by sectors 1809 byCurr = {} # distribution by currencies (include RUB) 1810 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1811 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1812 1813 for item in portfolioResponse["positions"]: 1814 self._figi = item["figi"] 1815 if not self._figi and item["ticker"]: 1816 self._ticker = item["ticker"] 1817 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1818 1819 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1820 1821 if instrument: 1822 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1823 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1824 1825 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1826 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1827 1828 else: 1829 blocked = 0 1830 1831 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1832 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1833 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1834 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1835 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1836 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1837 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1838 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1839 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1840 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1841 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1842 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1843 1844 statData = { 1845 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1846 "ticker": instrument["ticker"], # ticker by FIGI 1847 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1848 "volume": volume, # available volume of instrument 1849 "lots": lots, # volume in lots of instrument 1850 "direction": direction, # direction of an instrument's position: short or long 1851 "blocked": blocked, # blocked volume of currency or instrument 1852 "currentPrice": curPrice, # current instrument's price in basic asset 1853 "average": average, # current average position price 1854 "cost": cost, # current cost of all volume of instrument in basic asset 1855 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1856 "costRUB": costRUB, # cost of instrument in ruble 1857 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1858 "profit": profit, # expected profit at current moment 1859 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1860 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1861 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1862 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1863 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1864 "step": instrument["step"], # minimum price increment 1865 } 1866 1867 # adding distribution by unique countries: 1868 if statData["country"] not in byCountry.keys(): 1869 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1870 1871 else: 1872 byCountry[statData["country"]]["cost"] += costRUB 1873 byCountry[statData["country"]]["percent"] += percentCostRUB 1874 1875 if item["instrumentType"] != "currency": 1876 # adding distribution by unique companies: 1877 if statData["name"]: 1878 if statData["name"] not in byComp.keys(): 1879 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1880 1881 else: 1882 byComp[statData["name"]]["cost"] += costRUB 1883 byComp[statData["name"]]["percent"] += percentCostRUB 1884 1885 # adding distribution by unique sectors: 1886 if statData["sector"] not in bySect.keys(): 1887 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1888 1889 else: 1890 bySect[statData["sector"]]["cost"] += costRUB 1891 bySect[statData["sector"]]["percent"] += percentCostRUB 1892 1893 # adding distribution by unique currencies: 1894 if currency not in byCurr.keys(): 1895 byCurr[currency] = { 1896 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1897 "cost": costRUB, 1898 "percent": percentCostRUB 1899 } 1900 1901 else: 1902 byCurr[currency]["cost"] += costRUB 1903 byCurr[currency]["percent"] += percentCostRUB 1904 1905 # saving statistics for every instrument: 1906 if item["instrumentType"] == "currency": 1907 view["stat"]["Currencies"].append(statData) 1908 1909 # update dict with free funds for trading (total - blocked) by currencies 1910 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1911 view["stat"]["funds"][currency] = { 1912 "total": volume, 1913 "totalCostRUB": costRUB, # total volume cost in rubles 1914 "free": volume - blocked, 1915 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1916 } 1917 1918 elif item["instrumentType"] == "share": 1919 view["stat"]["Shares"].append(statData) 1920 1921 elif item["instrumentType"] == "bond": 1922 view["stat"]["Bonds"].append(statData) 1923 1924 elif item["instrumentType"] == "etf": 1925 view["stat"]["Etfs"].append(statData) 1926 1927 elif item["instrumentType"] == "Futures": 1928 view["stat"]["Futures"].append(statData) 1929 1930 else: 1931 continue 1932 1933 # total changes in Russian Ruble: 1934 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1935 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1936 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1937 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1938 view["stat"]["funds"]["rub"] = { 1939 "total": view["stat"]["availableRUB"], 1940 "totalCostRUB": view["stat"]["availableRUB"], 1941 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1942 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1943 } 1944 1945 # --- pending limit orders sector data: 1946 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1947 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1948 1949 for item in view["raw"]["orders"]: 1950 self._figi = item["figi"] 1951 1952 if item["figi"] not in uniquePendingOrdersFIGIs: 1953 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1954 1955 uniquePendingOrdersFIGIs.append(item["figi"]) 1956 uniquePendingOrders[item["figi"]] = instrument 1957 1958 else: 1959 instrument = uniquePendingOrders[item["figi"]] 1960 1961 if instrument: 1962 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1963 orderType = TKS_ORDER_TYPES[item["orderType"]] 1964 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1965 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1966 1967 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1968 if item["direction"] == "ORDER_DIRECTION_BUY": 1969 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1970 1971 else: 1972 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1973 1974 # requested price for order execution: 1975 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1976 1977 # necessary changes in percent to reach target from current price: 1978 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1979 1980 view["stat"]["orders"].append({ 1981 "orderID": item["orderId"], # orderId number parameter of current order 1982 "figi": item["figi"], # FIGI identification 1983 "ticker": instrument["ticker"], # ticker name by FIGI 1984 "lotsRequested": item["lotsRequested"], # requested lots value 1985 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1986 "currentPrice": lastPrice, # current instrument's price for defined action 1987 "targetPrice": target, # requested price for order execution in base currency 1988 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1989 "percentChanges": changes, # changes in percent to target from current price 1990 "currency": item["currency"], # instrument's currency name 1991 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1992 "type": orderType, # type of order from TKS_ORDER_TYPES 1993 "status": orderState, # order status from TKS_ORDER_STATES 1994 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1995 }) 1996 1997 # --- stop orders sector data: 1998 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1999 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2000 2001 for item in view["raw"]["stopOrders"]: 2002 self._figi = item["figi"] 2003 2004 if item["figi"] not in uniqueStopOrdersFIGIs: 2005 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2006 2007 uniqueStopOrdersFIGIs.append(item["figi"]) 2008 uniqueStopOrders[item["figi"]] = instrument 2009 2010 else: 2011 instrument = uniqueStopOrders[item["figi"]] 2012 2013 if instrument: 2014 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2015 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2016 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2017 2018 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2019 if "expirationTime" in item.keys(): 2020 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2021 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2022 2023 else: 2024 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2025 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2026 2027 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2028 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2029 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2030 2031 else: 2032 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2033 2034 # requested price when stop-order executed: 2035 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2036 2037 # price for limit-order, set up when stop-order executed: 2038 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2039 2040 # necessary changes in percent to reach target from current price: 2041 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2042 2043 view["stat"]["stopOrders"].append({ 2044 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2045 "figi": item["figi"], # FIGI identification 2046 "ticker": instrument["ticker"], # ticker name by FIGI 2047 "lotsRequested": item["lotsRequested"], # requested lots value 2048 "currentPrice": lastPrice, # current instrument's price for defined action 2049 "targetPrice": target, # requested price for stop-order execution in base currency 2050 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2051 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2052 "percentChanges": changes, # changes in percent to target from current price 2053 "currency": item["currency"], # instrument's currency name 2054 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2055 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2056 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2057 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2058 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2059 }) 2060 2061 # --- calculating data for analytics section: 2062 # portfolio distribution by assets: 2063 view["analytics"]["distrByAssets"] = { 2064 "Ruble": { 2065 "uniques": 1, 2066 "cost": view["stat"]["availableRUB"], 2067 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2068 }, 2069 "Currencies": { 2070 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2071 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2072 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2073 }, 2074 "Shares": { 2075 "uniques": len(view["stat"]["Shares"]), 2076 "cost": view["stat"]["sharesCostRUB"], 2077 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2078 }, 2079 "Bonds": { 2080 "uniques": len(view["stat"]["Bonds"]), 2081 "cost": view["stat"]["bondsCostRUB"], 2082 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2083 }, 2084 "Etfs": { 2085 "uniques": len(view["stat"]["Etfs"]), 2086 "cost": view["stat"]["etfsCostRUB"], 2087 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2088 }, 2089 "Futures": { 2090 "uniques": len(view["stat"]["Futures"]), 2091 "cost": view["stat"]["futuresCostRUB"], 2092 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2093 }, 2094 } 2095 2096 # portfolio distribution by companies: 2097 view["analytics"]["distrByCompanies"]["All money cash"] = { 2098 "ticker": "", 2099 "cost": view["stat"]["allCurrenciesCostRUB"], 2100 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2101 } 2102 view["analytics"]["distrByCompanies"].update(byComp) 2103 2104 # portfolio distribution by sectors: 2105 view["analytics"]["distrBySectors"]["All money cash"] = { 2106 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2107 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2108 } 2109 view["analytics"]["distrBySectors"].update(bySect) 2110 2111 # portfolio distribution by currencies: 2112 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2113 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2114 2115 if self.moreDebug: 2116 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2117 2118 view["analytics"]["distrByCurrencies"].update(byCurr) 2119 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2120 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2121 2122 # portfolio distribution by countries: 2123 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2124 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2125 2126 if self.moreDebug: 2127 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2128 2129 view["analytics"]["distrByCountries"].update(byCountry) 2130 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2131 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2132 2133 # --- Prepare text statistics overview in human-readable: 2134 if show or onlyFiles: 2135 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2136 2137 # Whatever the value `details`, header not changes: 2138 info = [ 2139 "# Client's portfolio\n\n", 2140 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2141 "* **Account ID:** [{}]\n".format(self.accountId), 2142 ] 2143 2144 if details in ["full", "positions", "digest"]: 2145 info.extend([ 2146 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2147 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2148 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2149 view["stat"]["totalChangesRUB"], 2150 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2151 view["stat"]["totalChangesPercentRUB"], 2152 ), 2153 ]) 2154 2155 if details in ["full", "positions"]: 2156 info.extend([ 2157 "## Open positions\n\n", 2158 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2159 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2160 "| **Ruble:** | {:>31} | | | | | |\n".format( 2161 "{:.2f} ({:.2f}) rub".format( 2162 view["stat"]["availableRUB"], 2163 view["stat"]["blockedRUB"], 2164 ) 2165 ) 2166 ]) 2167 2168 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2169 return [ 2170 "| | | | | | | |\n", 2171 "| {:<27} | | | | | {:>19} | |\n".format( 2172 noTradeStr if noTradeStr else typeStr, 2173 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2174 ), 2175 ] 2176 2177 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2178 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2179 "{} [{}]".format(data["ticker"], data["figi"]), 2180 "{:.2f} ({:.2f}) {}".format( 2181 data["volume"], 2182 data["blocked"], 2183 data["currency"], 2184 ) if isCurr else "{:.0f} ({:.0f})".format( 2185 data["volume"], 2186 data["blocked"], 2187 ), 2188 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2189 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2190 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2191 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2192 "{}{:.2f} {} ({}{:.2f}%)".format( 2193 "+" if data["profit"] > 0 else "", 2194 data["profit"], data["baseCurrencyName"], 2195 "+" if data["percentProfit"] > 0 else "", 2196 data["percentProfit"], 2197 ), 2198 ) 2199 2200 # --- Show currencies section: 2201 if view["stat"]["Currencies"]: 2202 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2203 for item in view["stat"]["Currencies"]: 2204 info.append(_InfoStr(item, isCurr=True)) 2205 2206 else: 2207 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2208 2209 # --- Show shares section: 2210 if view["stat"]["Shares"]: 2211 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2212 2213 for item in view["stat"]["Shares"]: 2214 info.append(_InfoStr(item)) 2215 2216 else: 2217 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2218 2219 # --- Show bonds section: 2220 if view["stat"]["Bonds"]: 2221 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2222 2223 for item in view["stat"]["Bonds"]: 2224 info.append(_InfoStr(item)) 2225 2226 else: 2227 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2228 2229 # --- Show etfs section: 2230 if view["stat"]["Etfs"]: 2231 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2232 2233 for item in view["stat"]["Etfs"]: 2234 info.append(_InfoStr(item)) 2235 2236 else: 2237 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2238 2239 # --- Show futures section: 2240 if view["stat"]["Futures"]: 2241 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2242 2243 for item in view["stat"]["Futures"]: 2244 info.append(_InfoStr(item)) 2245 2246 else: 2247 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2248 2249 if details in ["full", "orders"]: 2250 # --- Show pending limit orders section: 2251 if view["stat"]["orders"]: 2252 info.extend([ 2253 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2254 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2255 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2256 ]) 2257 2258 for item in view["stat"]["orders"]: 2259 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2260 "{} [{}]".format(item["ticker"], item["figi"]), 2261 item["orderID"], 2262 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2263 "{} {} ({}{:.2f}%)".format( 2264 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2265 item["baseCurrencyName"], 2266 "+" if item["percentChanges"] > 0 else "", 2267 float(item["percentChanges"]), 2268 ), 2269 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2270 item["action"], 2271 item["type"], 2272 item["date"], 2273 )) 2274 2275 else: 2276 info.append("\n## Total pending limit-orders: [0]\n") 2277 2278 # --- Show stop orders section: 2279 if view["stat"]["stopOrders"]: 2280 info.extend([ 2281 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2282 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2283 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2284 ]) 2285 2286 for item in view["stat"]["stopOrders"]: 2287 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2288 "{} [{}]".format(item["ticker"], item["figi"]), 2289 item["orderID"], 2290 item["lotsRequested"], 2291 "{} {} ({}{:.2f}%)".format( 2292 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2293 item["baseCurrencyName"], 2294 "+" if item["percentChanges"] > 0 else "", 2295 float(item["percentChanges"]), 2296 ), 2297 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2298 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2299 item["action"], 2300 item["type"], 2301 item["expType"], 2302 item["createDate"], 2303 item["expDate"], 2304 )) 2305 2306 else: 2307 info.append("\n## Total stop-orders: [0]\n") 2308 2309 if details in ["full", "analytics"]: 2310 # -- Show analytics section: 2311 if view["stat"]["portfolioCostRUB"] > 0: 2312 info.extend([ 2313 "\n# Analytics\n\n" 2314 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2315 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2316 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2317 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2318 view["stat"]["totalChangesRUB"], 2319 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2320 view["stat"]["totalChangesPercentRUB"], 2321 ), 2322 "\n## Portfolio distribution by assets\n" 2323 "\n| Type | Uniques | Percent | Current cost |\n", 2324 "|------------------------------------|---------|---------|--------------------|\n", 2325 ]) 2326 2327 for key in view["analytics"]["distrByAssets"].keys(): 2328 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2329 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2330 key, 2331 view["analytics"]["distrByAssets"][key]["uniques"], 2332 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2333 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2334 )) 2335 2336 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2337 2338 info.extend([ 2339 "\n## Portfolio distribution by companies\n" 2340 "\n| Company | Percent | Current cost |\n", 2341 aSepLine, 2342 ]) 2343 2344 for company in view["analytics"]["distrByCompanies"].keys(): 2345 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2346 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2347 "{}{}".format( 2348 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2349 company, 2350 ), 2351 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2352 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2353 )) 2354 2355 info.extend([ 2356 "\n## Portfolio distribution by sectors\n" 2357 "\n| Sector | Percent | Current cost |\n", 2358 aSepLine, 2359 ]) 2360 2361 for sector in view["analytics"]["distrBySectors"].keys(): 2362 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2363 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2364 sector, 2365 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2366 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2367 )) 2368 2369 info.extend([ 2370 "\n## Portfolio distribution by currencies\n" 2371 "\n| Instruments currencies | Percent | Current cost |\n", 2372 aSepLine, 2373 ]) 2374 2375 for curr in view["analytics"]["distrByCurrencies"].keys(): 2376 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2377 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2378 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2379 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2380 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2381 )) 2382 2383 info.extend([ 2384 "\n## Portfolio distribution by countries\n" 2385 "\n| Assets by country | Percent | Current cost |\n", 2386 aSepLine, 2387 ]) 2388 2389 for country in view["analytics"]["distrByCountries"].keys(): 2390 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2391 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2392 country, 2393 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2394 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2395 )) 2396 2397 if details in ["full", "calendar"]: 2398 # -- Show bonds payment calendar section: 2399 if view["stat"]["Bonds"]: 2400 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2401 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2402 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2403 2404 else: 2405 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2406 2407 infoText = "".join(info) 2408 2409 if show and not onlyFiles: 2410 uLogger.info(infoText) 2411 2412 if details == "full" and self.overviewFile: 2413 filename = self.overviewFile 2414 2415 elif details == "digest" and self.overviewDigestFile: 2416 filename = self.overviewDigestFile 2417 2418 elif details == "positions" and self.overviewPositionsFile: 2419 filename = self.overviewPositionsFile 2420 2421 elif details == "orders" and self.overviewOrdersFile: 2422 filename = self.overviewOrdersFile 2423 2424 elif details == "analytics" and self.overviewAnalyticsFile: 2425 filename = self.overviewAnalyticsFile 2426 2427 elif details == "calendar" and self.overviewBondsCalendarFile: 2428 filename = self.overviewBondsCalendarFile 2429 2430 else: 2431 filename = "" 2432 2433 if filename and (show or onlyFiles): 2434 with open(filename, "w", encoding="UTF-8") as fH: 2435 fH.write(infoText) 2436 2437 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2438 2439 if self.useHTMLReports: 2440 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2441 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2442 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2443 2444 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2445 2446 return view 2447 2448 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2449 """ 2450 Returns history operations between two given dates for current `accountId`. 2451 If `reportFile` string is not empty then also save human-readable report. 2452 Shows some statistical data of closed positions. 2453 2454 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2455 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2456 :param show: if `True` then also prints all records to the console. 2457 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2458 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2459 :return: original list of dictionaries with history of deals records from API ("operations" key): 2460 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2461 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2462 """ 2463 if self.accountId is None or not self.accountId: 2464 uLogger.error("Variable `accountId` must be defined for using this method!") 2465 raise Exception("Account ID required") 2466 2467 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2468 2469 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2470 2471 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2472 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2473 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2474 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2475 customStat = {} # custom statistics in additional to responseJSON 2476 2477 # --- output report in human-readable format: 2478 if self.reportFile and (show or onlyFiles): 2479 splitLine1 = "| | | | | |\n" # Summary section 2480 splitLine2 = "| | | | | | | | |\n" # Operations section 2481 nextDay = "" 2482 2483 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2484 2485 if len(ops) > 0: 2486 customStat = { 2487 "opsCount": 0, # total operations count 2488 "buyCount": 0, # buy operations 2489 "sellCount": 0, # sell operations 2490 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2491 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2492 "payIn": {"rub": 0.}, # Deposit brokerage account 2493 "payOut": {"rub": 0.}, # Withdrawals 2494 "divs": {"rub": 0.}, # Dividends income 2495 "coupons": {"rub": 0.}, # Coupon's income 2496 "brokerCom": {"rub": 0.}, # Service commissions 2497 "serviceCom": {"rub": 0.}, # Service commissions 2498 "marginCom": {"rub": 0.}, # Margin commissions 2499 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2500 } 2501 2502 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2503 for item in ops: 2504 if item["state"] == "OPERATION_STATE_EXECUTED": 2505 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2506 2507 # count buy operations: 2508 if "_BUY" in item["operationType"]: 2509 customStat["buyCount"] += 1 2510 2511 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2512 customStat["buyTotal"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["buyTotal"][item["payment"]["currency"]] = payment 2516 2517 # count sell operations: 2518 elif "_SELL" in item["operationType"]: 2519 customStat["sellCount"] += 1 2520 2521 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2522 customStat["sellTotal"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["sellTotal"][item["payment"]["currency"]] = payment 2526 2527 # count incoming operations: 2528 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2529 if item["payment"]["currency"] in customStat["payIn"].keys(): 2530 customStat["payIn"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["payIn"][item["payment"]["currency"]] = payment 2534 2535 # count withdrawals operations: 2536 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2537 if item["payment"]["currency"] in customStat["payOut"].keys(): 2538 customStat["payOut"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["payOut"][item["payment"]["currency"]] = payment 2542 2543 # count dividends income: 2544 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2545 if item["payment"]["currency"] in customStat["divs"].keys(): 2546 customStat["divs"][item["payment"]["currency"]] += payment 2547 2548 else: 2549 customStat["divs"][item["payment"]["currency"]] = payment 2550 2551 # count coupon's income: 2552 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2553 if item["payment"]["currency"] in customStat["coupons"].keys(): 2554 customStat["coupons"][item["payment"]["currency"]] += payment 2555 2556 else: 2557 customStat["coupons"][item["payment"]["currency"]] = payment 2558 2559 # count broker commissions: 2560 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2561 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2562 customStat["brokerCom"][item["payment"]["currency"]] += payment 2563 2564 else: 2565 customStat["brokerCom"][item["payment"]["currency"]] = payment 2566 2567 # count service commissions: 2568 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2569 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2570 customStat["serviceCom"][item["payment"]["currency"]] += payment 2571 2572 else: 2573 customStat["serviceCom"][item["payment"]["currency"]] = payment 2574 2575 # count margin commissions: 2576 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2577 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2578 customStat["marginCom"][item["payment"]["currency"]] += payment 2579 2580 else: 2581 customStat["marginCom"][item["payment"]["currency"]] = payment 2582 2583 # count withholding taxes: 2584 elif "_TAX" in item["operationType"]: 2585 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2586 customStat["allTaxes"][item["payment"]["currency"]] += payment 2587 2588 else: 2589 customStat["allTaxes"][item["payment"]["currency"]] = payment 2590 2591 else: 2592 continue 2593 2594 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2595 2596 # --- view "Actions" lines: 2597 info.extend([ 2598 "| Report sections | | | | |\n", 2599 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2600 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2601 "| | Buy: {:<22} | {:<28} | | |\n".format( 2602 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2603 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2604 ), 2605 "| | Sell: {:<21} | {:<28} | | |\n".format( 2606 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2607 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2608 ), 2609 ]) 2610 2611 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2612 for key in opsKeys: 2613 if key == "rub": 2614 continue 2615 2616 info.extend([ 2617 "| | | {:<28} | | |\n".format( 2618 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2619 ), 2620 "| | | {:<28} | | |\n".format( 2621 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2622 ), 2623 ]) 2624 2625 info.append(splitLine1) 2626 2627 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2628 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2629 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2630 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2631 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2632 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2633 ) 2634 2635 # --- view "Payments" lines: 2636 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2637 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2638 2639 for key in paymentsKeys: 2640 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2641 2642 info.append(splitLine1) 2643 2644 # --- view "Commissions and taxes" lines: 2645 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2646 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2647 2648 for key in comKeys: 2649 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2650 2651 info.extend([ 2652 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2653 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2654 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2655 ]) 2656 2657 else: 2658 info.append("Broker returned no operations during this period\n") 2659 2660 # --- view "Operations" section: 2661 for item in ops: 2662 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2663 continue 2664 2665 else: 2666 self._figi = item["figi"] 2667 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2668 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2669 2670 # group of deals during one day: 2671 if nextDay and item["date"].split("T")[0] != nextDay: 2672 info.append(splitLine2) 2673 nextDay = "" 2674 2675 else: 2676 nextDay = item["date"].split("T")[0] # saving current day for splitting 2677 2678 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2679 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2680 self._figi if self._figi else "—", 2681 instrument["ticker"] if instrument else "—", 2682 instrument["type"] if instrument else "—", 2683 item["quantity"] if int(item["quantity"]) > 0 else "—", 2684 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2685 TKS_OPERATION_STATES[item["state"]], 2686 TKS_OPERATION_TYPES[item["operationType"]], 2687 )) 2688 2689 infoText = "".join(info) 2690 2691 if show and not onlyFiles: 2692 if self.moreDebug: 2693 uLogger.debug("Records about history of a client's operations successfully received") 2694 2695 uLogger.info(infoText) 2696 2697 if self.reportFile and (show or onlyFiles): 2698 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2699 fH.write(infoText) 2700 2701 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2702 2703 if self.useHTMLReports: 2704 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2705 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2706 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2707 2708 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2709 2710 return ops, customStat 2711 2712 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2713 """ 2714 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2715 2716 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2717 Warning! Broker server used ISO UTC time by default. 2718 2719 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2720 Also, `historyFile` used to update history with `onlyMissing` parameter. 2721 2722 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2723 2724 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2725 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2726 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2727 `"hour"`, `"day"`. Default: `"hour"`. 2728 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2729 False by default. Warning! History appends only from last candle to current time 2730 with always update last candle! 2731 :param csvSep: separator if csv-file is used, `,` by default. 2732 :param show: if `True` then also prints Pandas DataFrame to the console. 2733 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2734 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2735 `["date", "time", "open", "high", "low", "close", "volume"]`. 2736 """ 2737 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2738 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2739 history = None # empty pandas object for history 2740 2741 if interval not in TKS_CANDLE_INTERVALS.keys(): 2742 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2743 raise Exception("Incorrect value") 2744 2745 if not (self._ticker or self._figi): 2746 uLogger.error("Ticker or FIGI must be defined!") 2747 raise Exception("Ticker or FIGI required") 2748 2749 if self._ticker and not self._figi: 2750 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2751 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2752 2753 if self._figi and not self._ticker: 2754 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2755 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2756 2757 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2758 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2759 if interval.lower() != "day": 2760 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2761 2762 delta = dtEnd - dtStart # current UTC time minus last time in file 2763 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2764 2765 # calculate history length in candles: 2766 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2767 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2768 length += 1 # to avoid fraction time 2769 2770 # calculate data blocks count: 2771 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2772 2773 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2774 if self.moreDebug: 2775 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2776 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2777 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2778 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2779 2780 tempOld = None # pandas object for old history, if --only-missing key present 2781 lastTime = None # datetime object of last old candle in file 2782 2783 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2784 if self.moreDebug: 2785 uLogger.debug("--only-missing key present, add only last missing candles...") 2786 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2787 2788 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2789 2790 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2791 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2792 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2793 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2794 2795 # get last datetime object from last string in file or minus 1 delta if file is empty: 2796 if len(tempOld) > 0: 2797 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2798 2799 else: 2800 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2801 2802 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2803 2804 responseJSONs = [] # raw history blocks of data 2805 2806 blockEnd = dtEnd 2807 for item in range(blocks): 2808 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2809 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2810 2811 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2812 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2813 )) 2814 2815 if blockStart == blockEnd: 2816 uLogger.debug("Skipped this zero-length block...") 2817 2818 else: 2819 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2820 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2821 self.body = str({ 2822 "figi": self._figi, 2823 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2824 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2825 "interval": TKS_CANDLE_INTERVALS[interval][0] 2826 }) 2827 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2828 2829 if "code" in responseJSON.keys(): 2830 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2831 2832 else: 2833 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2834 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2835 2836 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2837 2838 blockEnd = blockStart 2839 2840 printCount = len(responseJSONs) # candles to show in console 2841 if responseJSONs: 2842 tempHistory = pd.DataFrame( 2843 data={ 2844 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2845 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2846 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2847 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2848 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2849 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2850 "volume": [int(item["volume"]) for item in responseJSONs], 2851 }, 2852 index=range(len(responseJSONs)), 2853 columns=["date", "time", "open", "high", "low", "close", "volume"], 2854 ) 2855 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2856 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2857 2858 # append only newest candles to old history if --only-missing key present: 2859 if onlyMissing and tempOld is not None and lastTime is not None: 2860 index = 0 # find start index in tempHistory data: 2861 2862 for i, item in tempHistory.iterrows(): 2863 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2864 2865 if curTime == lastTime: 2866 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2867 index = i 2868 printCount = index + 1 2869 break 2870 2871 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2872 2873 else: 2874 history = tempHistory # if no `--only-missing` key then load full data from server 2875 2876 if self.moreDebug: 2877 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2878 2879 if history is not None and not history.empty: 2880 if show and not onlyFiles: 2881 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2882 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2883 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2884 )) 2885 2886 else: 2887 uLogger.warning("Received an empty candles history!") 2888 2889 if self.historyFile is not None: 2890 if history is not None and not history.empty: 2891 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2892 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2893 2894 else: 2895 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2896 2897 else: 2898 if self.moreDebug: 2899 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2900 2901 return history 2902 2903 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2904 """ 2905 Load candles history from csv-file and return Pandas DataFrame object. 2906 2907 See also: `History()` and `ShowHistoryChart()` methods. 2908 2909 :param filePath: path to csv-file to open. 2910 """ 2911 loadedHistory = None # init candles data object 2912 2913 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2914 2915 if os.path.exists(filePath): 2916 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2917 2918 tfStr = self.priceModel.FormattedDelta( 2919 self.priceModel.timeframe, 2920 "{days} days {hours}h {minutes}m {seconds}s", 2921 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2922 self.priceModel.timeframe, 2923 "{hours}h {minutes}m {seconds}s", 2924 ) 2925 2926 if loadedHistory is not None and not loadedHistory.empty: 2927 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2928 len(loadedHistory), 2929 tfStr, 2930 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2931 ) 2932 2933 else: 2934 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2935 2936 else: 2937 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2938 2939 return loadedHistory 2940 2941 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2942 """ 2943 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2944 2945 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2946 Default: `index.html` (both for interact and non-interact candlesticks chart). 2947 2948 See also: `History()` and `LoadHistory()` methods. 2949 2950 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2951 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2952 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2953 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2954 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2955 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2956 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2957 """ 2958 if isinstance(candles, str): 2959 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2960 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2961 2962 elif isinstance(candles, pd.DataFrame): 2963 self.priceModel.prices = candles # set candles chain from variable 2964 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2965 2966 if "datetime" not in candles.columns: 2967 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2968 2969 else: 2970 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2971 raise Exception("Incorrect value") 2972 2973 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2974 2975 if interact: 2976 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2977 2978 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2979 2980 else: 2981 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2982 2983 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2984 2985 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2986 2987 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2988 """ 2989 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2990 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2991 2992 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2993 2994 :param operation: string "Buy" or "Sell". 2995 :param lots: volume, integer count of lots >= 1. 2996 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2997 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2998 :param expDate: string "Undefined" by default or local date in future, 2999 it is a string with format `%Y-%m-%d %H:%M:%S`. 3000 :return: JSON with response from broker server. 3001 """ 3002 if self.accountId is None or not self.accountId: 3003 uLogger.error("Variable `accountId` must be defined for using this method!") 3004 raise Exception("Account ID required") 3005 3006 if operation is None or not operation or operation not in ("Buy", "Sell"): 3007 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3008 raise Exception("Incorrect value") 3009 3010 if lots is None or lots < 1: 3011 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3012 lots = 1 3013 3014 if tp is None or tp < 0: 3015 tp = 0 3016 3017 if sl is None or sl < 0: 3018 sl = 0 3019 3020 if expDate is None or not expDate: 3021 expDate = "Undefined" 3022 3023 if not (self._ticker or self._figi): 3024 uLogger.error("Ticker or FIGI must be defined!") 3025 raise Exception("Ticker or FIGI required") 3026 3027 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3028 self._ticker = instrument["ticker"] 3029 self._figi = instrument["figi"] 3030 3031 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3032 3033 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3034 self.body = str({ 3035 "figi": self._figi, 3036 "quantity": str(lots), 3037 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3038 "accountId": str(self.accountId), 3039 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3040 }) 3041 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3042 3043 if "orderId" in response.keys(): 3044 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3045 operation, response["orderId"], 3046 self._ticker, self._figi, lots, 3047 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3048 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3049 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3050 )) 3051 3052 if tp > 0: 3053 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3054 3055 if sl > 0: 3056 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3057 3058 else: 3059 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3060 3061 return response 3062 3063 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3064 """ 3065 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3066 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3067 3068 See also: `Order()` and `Trade()` docstrings. 3069 3070 :param lots: volume, integer count of lots >= 1. 3071 :param tp: float > 0, take profit price of stop-order. 3072 :param sl: float > 0, stop loss price of stop-order. 3073 :param expDate: it's a local date in future. 3074 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3075 :return: JSON with response from broker server. 3076 """ 3077 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3078 3079 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3080 """ 3081 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3082 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3083 3084 See also: `Order()` and `Trade()` docstrings. 3085 3086 :param lots: volume, integer count of lots >= 1. 3087 :param tp: float > 0, take profit price of stop-order. 3088 :param sl: float > 0, stop loss price of stop-order. 3089 :param expDate: it's a local date in the future. 3090 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3091 :return: JSON with response from broker server. 3092 """ 3093 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3094 3095 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3096 """ 3097 Close position of given instruments. 3098 3099 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3100 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3101 This avoids unnecessary downloading data from the server. 3102 """ 3103 if instruments is None or not instruments: 3104 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3105 raise Exception("Ticker or FIGI required") 3106 3107 if isinstance(instruments, str): 3108 instruments = [instruments] 3109 3110 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3111 if uniqueInstruments: 3112 if portfolio is None or not portfolio: 3113 portfolio = self.Overview(show=False) 3114 3115 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3116 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3117 3118 for self._figi in uniqueInstruments: 3119 if self._figi not in allOpened: 3120 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3121 continue 3122 3123 # search open trade info about instrument by ticker: 3124 instrument = {} 3125 for iType in TKS_INSTRUMENTS: 3126 if instrument: 3127 break 3128 3129 for item in portfolio["stat"][iType]: 3130 if item["figi"] == self._figi: 3131 instrument = item 3132 break 3133 3134 if instrument: 3135 self._ticker = instrument["ticker"] 3136 self._figi = instrument["figi"] 3137 3138 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3139 self._ticker, 3140 self._figi, 3141 int(instrument["volume"]), 3142 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3143 )) 3144 3145 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3146 3147 if tradeLots > 0: 3148 if instrument["blocked"] > 0: 3149 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3150 instrument["blocked"], 3151 self._ticker, 3152 tradeLots, 3153 )) 3154 3155 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3156 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3157 3158 else: 3159 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3160 3161 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3162 """ 3163 Close all positions of given instruments with defined type. 3164 3165 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3166 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3167 This avoids unnecessary downloading data from the server. 3168 """ 3169 if iType not in TKS_INSTRUMENTS: 3170 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3171 3172 else: 3173 if portfolio is None or not portfolio: 3174 portfolio = self.Overview(show=False) 3175 3176 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3177 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3178 3179 if tickers and portfolio: 3180 self.CloseTrades(tickers, portfolio) 3181 3182 else: 3183 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3184 3185 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3186 """ 3187 Universal method to create market or limit orders with all available parameters for current `accountId`. 3188 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3189 3190 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3191 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3192 3193 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3194 then broker immediately open market order as you can do simple --buy or --sell operations! 3195 3196 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3197 When current price will go up or down to target price value then broker opens a limit order. 3198 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3199 3200 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3201 3202 :param operation: string "Buy" or "Sell". 3203 :param orderType: string "Limit" or "Stop". 3204 :param lots: volume, integer count of lots >= 1. 3205 :param targetPrice: target price > 0. This is open trade price for limit order. 3206 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3207 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3208 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3209 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3210 Stop loss order always executed by market price. 3211 :param expDate: string "Undefined" by default or local date in future. 3212 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3213 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3214 A limit order has no expiration date, it lasts until the end of the trading day. 3215 :return: JSON with response from broker server. 3216 """ 3217 if self.accountId is None or not self.accountId: 3218 uLogger.error("Variable `accountId` must be defined for using this method!") 3219 raise Exception("Account ID required") 3220 3221 if operation is None or not operation or operation not in ("Buy", "Sell"): 3222 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3223 raise Exception("Incorrect value") 3224 3225 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3226 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3227 raise Exception("Incorrect value") 3228 3229 if lots is None or lots < 1: 3230 uLogger.error("You must define trade volume > 0: integer count of lots!") 3231 raise Exception("Incorrect value") 3232 3233 if targetPrice is None or targetPrice <= 0: 3234 uLogger.error("Target price for limit-order must be greater than 0!") 3235 raise Exception("Incorrect value") 3236 3237 if limitPrice is None or limitPrice <= 0: 3238 limitPrice = targetPrice 3239 3240 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3241 stopType = "Limit" 3242 3243 if expDate is None or not expDate: 3244 expDate = "Undefined" 3245 3246 if not (self._ticker or self._figi): 3247 uLogger.error("Tocker or FIGI must be defined!") 3248 raise Exception("Ticker or FIGI required") 3249 3250 response = {} 3251 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3252 self._ticker = instrument["ticker"] 3253 self._figi = instrument["figi"] 3254 3255 if orderType == "Limit": 3256 uLogger.debug( 3257 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3258 self._ticker, self._figi, 3259 operation, lots, targetPrice, instrument["currency"], 3260 )) 3261 3262 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3263 self.body = str({ 3264 "figi": self._figi, 3265 "quantity": str(lots), 3266 "price": FloatToNano(targetPrice), 3267 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3268 "accountId": str(self.accountId), 3269 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3270 }) 3271 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3272 3273 if "orderId" in response.keys(): 3274 uLogger.info( 3275 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3276 response["orderId"], self._ticker, self._figi, operation, lots, 3277 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3278 )) 3279 3280 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3281 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3282 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3283 targetPrice, instrument["currency"], 3284 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3285 )) 3286 3287 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3288 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3289 targetPrice, instrument["currency"], 3290 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3291 )) 3292 3293 else: 3294 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3295 3296 if orderType == "Stop": 3297 uLogger.debug( 3298 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3299 self._ticker, self._figi, 3300 operation, lots, 3301 targetPrice, instrument["currency"], 3302 limitPrice, instrument["currency"], 3303 stopType, expDate, 3304 )) 3305 3306 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3307 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3308 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3309 3310 body = { 3311 "figi": self._figi, 3312 "quantity": str(lots), 3313 "price": FloatToNano(limitPrice), 3314 "stopPrice": FloatToNano(targetPrice), 3315 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3316 "accountId": str(self.accountId), 3317 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3318 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3319 } 3320 3321 if expDateUTC: 3322 body["expireDate"] = expDateUTC 3323 3324 self.body = str(body) 3325 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3326 3327 if "stopOrderId" in response.keys(): 3328 uLogger.info( 3329 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3330 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3331 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3332 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3333 TKS_STOP_ORDER_TYPES[stopOrderType], 3334 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3335 )) 3336 3337 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3338 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3339 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3340 targetPrice, instrument["currency"], 3341 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3342 )) 3343 3344 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3345 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3346 targetPrice, instrument["currency"], 3347 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3348 )) 3349 3350 else: 3351 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3352 3353 return response 3354 3355 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3356 """ 3357 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3358 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3359 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3360 See also: `Order()` docstring. 3361 3362 :param lots: volume, integer count of lots >= 1. 3363 :param targetPrice: target price > 0. This is open trade price for limit order. 3364 :return: JSON with response from broker server. 3365 """ 3366 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3367 3368 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3369 """ 3370 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3371 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3372 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3373 target price value then broker opens a limit order. See also: `Order()` docstring. 3374 3375 :param lots: volume, integer count of lots >= 1. 3376 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3377 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3378 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3379 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3380 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3381 :param expDate: string "Undefined" by default or local date in future. 3382 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3383 This date is converting to UTC format for server. 3384 :return: JSON with response from broker server. 3385 """ 3386 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3387 3388 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3389 """ 3390 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3391 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3392 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3393 See also: `Order()` docstring. 3394 3395 :param lots: volume, integer count of lots >= 1. 3396 :param targetPrice: target price > 0. This is open trade price for limit order. 3397 :return: JSON with response from broker server. 3398 """ 3399 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3400 3401 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3402 """ 3403 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3404 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3405 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3406 target price value then broker opens a limit order. See also: `Order()` docstring. 3407 3408 :param lots: volume, integer count of lots >= 1. 3409 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3410 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3411 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3412 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3413 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3414 :param expDate: string "Undefined" by default or local date in future. 3415 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3416 This date is converting to UTC format for server. 3417 :return: JSON with response from broker server. 3418 """ 3419 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3420 3421 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3422 """ 3423 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3424 3425 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3426 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3427 This avoids unnecessary downloading data from the server. 3428 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3429 """ 3430 if self.accountId is None or not self.accountId: 3431 uLogger.error("Variable `accountId` must be defined for using this method!") 3432 raise Exception("Account ID required") 3433 3434 if orderIDs: 3435 if allOrdersIDs is None: 3436 rawOrders = self.RequestPendingOrders() 3437 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3438 3439 if allStopOrdersIDs is None: 3440 rawStopOrders = self.RequestStopOrders() 3441 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3442 3443 for orderID in orderIDs: 3444 idInPendingOrders = orderID in allOrdersIDs 3445 idInStopOrders = orderID in allStopOrdersIDs 3446 3447 if not (idInPendingOrders or idInStopOrders): 3448 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3449 continue 3450 3451 else: 3452 if idInPendingOrders: 3453 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3454 3455 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3456 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3457 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3458 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3459 3460 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3461 if self.moreDebug: 3462 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3463 3464 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3465 3466 else: 3467 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3468 3469 elif idInStopOrders: 3470 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3471 3472 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3473 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3474 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3475 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3476 3477 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3478 if self.moreDebug: 3479 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3480 3481 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3482 3483 else: 3484 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3485 3486 else: 3487 continue 3488 3489 def CloseAllOrders(self) -> None: 3490 """ 3491 Gets a list of open pending and stop orders and cancel it all. 3492 """ 3493 rawOrders = self.RequestPendingOrders() 3494 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3495 lenOrders = len(allOrdersIDs) 3496 3497 rawStopOrders = self.RequestStopOrders() 3498 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3499 lenSOrders = len(allStopOrdersIDs) 3500 3501 if lenOrders > 0 or lenSOrders > 0: 3502 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3503 3504 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3505 3506 else: 3507 uLogger.info("Orders not found, nothing to cancel.") 3508 3509 def CloseAll(self, *args) -> None: 3510 """ 3511 Close all available (not blocked) opened trades and orders. 3512 3513 Also, you can select one or more keywords case-insensitive: 3514 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3515 3516 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3517 """ 3518 overview = self.Overview(show=False) # get all open trades info 3519 3520 if len(args) == 0: 3521 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3522 self.CloseAllOrders() # close all pending and stop orders 3523 3524 for iType in TKS_INSTRUMENTS: 3525 if iType != "Currencies": 3526 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3527 3528 else: 3529 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3530 lowerArgs = [x.lower() for x in args] 3531 3532 if "orders" in lowerArgs: 3533 self.CloseAllOrders() # close all pending and stop orders 3534 3535 for iType in TKS_INSTRUMENTS: 3536 if iType.lower() in lowerArgs and iType != "Currencies": 3537 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3538 3539 def CloseAllByTicker(self, instrument: str) -> None: 3540 """ 3541 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3542 3543 This method searches opened trade and orders of instrument throw all portfolio and then use 3544 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3545 3546 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3547 3548 :param instrument: string with ticker. 3549 """ 3550 if instrument is None or not instrument: 3551 uLogger.error("Ticker name must be defined for using this method!") 3552 raise Exception("Ticker required") 3553 3554 overview = self.Overview(show=False) # get user portfolio with all open trades info 3555 3556 self._ticker = instrument # try to set instrument as ticker 3557 self._figi = "" 3558 3559 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3560 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3561 3562 if limitAll and self.IsInLimitOrders(portfolio=overview): 3563 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3564 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3565 3566 if stopAll and self.IsInStopOrders(portfolio=overview): 3567 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3568 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3569 3570 if self.IsInPortfolio(portfolio=overview): 3571 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3572 self.CloseTrades(instruments=[instrument], portfolio=overview) 3573 3574 def CloseAllByFIGI(self, instrument: str) -> None: 3575 """ 3576 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3577 3578 This method searches opened trade and orders of instrument throw all portfolio and then use 3579 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3580 3581 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3582 3583 :param instrument: string with FIGI id. 3584 """ 3585 if instrument is None or not instrument: 3586 uLogger.error("FIGI id must be defined for using this method!") 3587 raise Exception("FIGI required") 3588 3589 overview = self.Overview(show=False) # get user portfolio with all open trades info 3590 3591 self._ticker = "" 3592 self._figi = instrument # try to set instrument as FIGI id 3593 3594 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3595 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3596 3597 if limitAll and self.IsInLimitOrders(portfolio=overview): 3598 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3599 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3600 3601 if stopAll and self.IsInStopOrders(portfolio=overview): 3602 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3603 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3604 3605 if self.IsInPortfolio(portfolio=overview): 3606 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3607 self.CloseTrades(instruments=[instrument], portfolio=overview) 3608 3609 @staticmethod 3610 def ParseOrderParameters(operation, **inputParameters): 3611 """ 3612 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3613 3614 :param operation: string "Buy" or "Sell". 3615 :param inputParameters: this is dict of strings that looks like this 3616 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3617 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3618 "prices" key: one or more prices to open limit-orders 3619 Counts of values in lots and prices lists must be equals! 3620 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3621 """ 3622 # TODO: update order grid work with api v2 3623 pass 3624 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3625 # 3626 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3627 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3628 # raise Exception("Incorrect value") 3629 # 3630 # if "l" in inputParameters.keys(): 3631 # inputParameters["lots"] = inputParameters.pop("l") 3632 # 3633 # if "p" in inputParameters.keys(): 3634 # inputParameters["prices"] = inputParameters.pop("p") 3635 # 3636 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3637 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3638 # raise Exception("Incorrect value") 3639 # 3640 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3641 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3642 # 3643 # if len(lots) != len(prices): 3644 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3645 # raise Exception("Incorrect value") 3646 # 3647 # uLogger.debug("Extracted parameters for orders:") 3648 # uLogger.debug("lots = {}".format(lots)) 3649 # uLogger.debug("prices = {}".format(prices)) 3650 # 3651 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3652 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3653 # uLogger.debug("Order parameters: {}".format(result)) 3654 # 3655 # return result 3656 3657 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3658 """ 3659 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3660 3661 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3662 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3663 """ 3664 result = False 3665 msg = "Instrument not defined!" 3666 3667 if portfolio is None or not portfolio: 3668 portfolio = self.Overview(show=False) 3669 3670 if self._ticker: 3671 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3672 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3673 3674 for iType in TKS_INSTRUMENTS: 3675 for instrument in portfolio["stat"][iType]: 3676 if instrument["ticker"] == self._ticker: 3677 result = True 3678 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3679 break 3680 3681 elif self._figi: 3682 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3683 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3684 3685 for iType in TKS_INSTRUMENTS: 3686 for instrument in portfolio["stat"][iType]: 3687 if instrument["figi"] == self._figi: 3688 result = True 3689 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3690 break 3691 3692 else: 3693 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3694 3695 uLogger.debug(msg) 3696 3697 return result 3698 3699 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3700 """ 3701 Returns instrument from the user's portfolio if it presents there. 3702 Instrument must be defined by `ticker` (highly priority) or `figi`. 3703 3704 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3705 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3706 """ 3707 result = None 3708 msg = "Instrument not defined!" 3709 3710 if portfolio is None or not portfolio: 3711 portfolio = self.Overview(show=False) 3712 3713 if self._ticker: 3714 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3715 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3716 3717 for iType in TKS_INSTRUMENTS: 3718 for instrument in portfolio["stat"][iType]: 3719 if instrument["ticker"] == self._ticker: 3720 result = instrument 3721 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3722 break 3723 3724 elif self._figi: 3725 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3726 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3727 3728 for iType in TKS_INSTRUMENTS: 3729 for instrument in portfolio["stat"][iType]: 3730 if instrument["figi"] == self._figi: 3731 result = instrument 3732 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3733 break 3734 3735 else: 3736 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3737 3738 uLogger.debug(msg) 3739 3740 return result 3741 3742 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3743 """ 3744 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3745 3746 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3747 3748 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3749 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3750 """ 3751 result = False 3752 msg = "Instrument not defined!" 3753 3754 if portfolio is None or not portfolio: 3755 portfolio = self.Overview(show=False) 3756 3757 if self._ticker: 3758 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3759 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3760 3761 for instrument in portfolio["stat"]["orders"]: 3762 if instrument["ticker"] == self._ticker: 3763 result = True 3764 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3765 break 3766 3767 elif self._figi: 3768 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3769 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3770 3771 for instrument in portfolio["stat"]["orders"]: 3772 if instrument["figi"] == self._figi: 3773 result = True 3774 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3775 break 3776 3777 else: 3778 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3779 3780 uLogger.debug(msg) 3781 3782 return result 3783 3784 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3785 """ 3786 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3787 Instrument must be defined by `ticker` (highly priority) or `figi`. 3788 3789 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3790 3791 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3792 :return: list with `orderID`s of limit orders. 3793 """ 3794 result = [] 3795 msg = "Instrument not defined!" 3796 3797 if portfolio is None or not portfolio: 3798 portfolio = self.Overview(show=False) 3799 3800 if self._ticker: 3801 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3802 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3803 3804 for instrument in portfolio["stat"]["orders"]: 3805 if instrument["ticker"] == self._ticker: 3806 result.append(instrument["orderID"]) 3807 3808 if result: 3809 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3810 3811 elif self._figi: 3812 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3813 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3814 3815 for instrument in portfolio["stat"]["orders"]: 3816 if instrument["figi"] == self._figi: 3817 result.append(instrument["orderID"]) 3818 3819 if result: 3820 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3821 3822 else: 3823 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3824 3825 uLogger.debug(msg) 3826 3827 return result 3828 3829 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3830 """ 3831 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3832 3833 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3834 3835 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3836 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3837 """ 3838 result = False 3839 msg = "Instrument not defined!" 3840 3841 if portfolio is None or not portfolio: 3842 portfolio = self.Overview(show=False) 3843 3844 if self._ticker: 3845 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3846 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3847 3848 for instrument in portfolio["stat"]["stopOrders"]: 3849 if instrument["ticker"] == self._ticker: 3850 result = True 3851 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3852 break 3853 3854 elif self._figi: 3855 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3856 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3857 3858 for instrument in portfolio["stat"]["stopOrders"]: 3859 if instrument["figi"] == self._figi: 3860 result = True 3861 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3862 break 3863 3864 else: 3865 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3866 3867 uLogger.debug(msg) 3868 3869 return result 3870 3871 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3872 """ 3873 Returns list with all `orderID`s of opened stop orders for the instrument. 3874 Instrument must be defined by `ticker` (highly priority) or `figi`. 3875 3876 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3877 3878 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3879 :return: list with `orderID`s of stop orders. 3880 """ 3881 result = [] 3882 msg = "Instrument not defined!" 3883 3884 if portfolio is None or not portfolio: 3885 portfolio = self.Overview(show=False) 3886 3887 if self._ticker: 3888 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3889 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3890 3891 for instrument in portfolio["stat"]["stopOrders"]: 3892 if instrument["ticker"] == self._ticker: 3893 result.append(instrument["orderID"]) 3894 3895 if result: 3896 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3897 3898 elif self._figi: 3899 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3900 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3901 3902 for instrument in portfolio["stat"]["stopOrders"]: 3903 if instrument["figi"] == self._figi: 3904 result.append(instrument["orderID"]) 3905 3906 if result: 3907 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3908 3909 else: 3910 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3911 3912 uLogger.debug(msg) 3913 3914 return result 3915 3916 def RequestLimits(self) -> dict: 3917 """ 3918 Method for obtaining the available funds for withdrawal for current `accountId`. 3919 3920 See also: 3921 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3922 - `OverviewLimits()` method 3923 3924 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3925 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3926 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3927 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3928 """ 3929 if self.accountId is None or not self.accountId: 3930 uLogger.error("Variable `accountId` must be defined for using this method!") 3931 raise Exception("Account ID required") 3932 3933 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3934 3935 self.body = str({"accountId": self.accountId}) 3936 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3937 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3938 3939 if self.moreDebug: 3940 uLogger.debug("Records about available funds for withdrawal successfully received") 3941 3942 return rawLimits 3943 3944 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3945 """ 3946 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3947 3948 See also: `RequestLimits()`. 3949 3950 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3951 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3952 :return: dict with raw parsed data from server and some calculated statistics about it. 3953 """ 3954 if self.accountId is None or not self.accountId: 3955 uLogger.error("Variable `accountId` must be defined for using this method!") 3956 raise Exception("Account ID required") 3957 3958 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3959 3960 view = { 3961 "rawLimits": rawLimits, 3962 "limits": { # parsed data for every currency: 3963 "money": { # this is an array of portfolio currency positions 3964 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3965 }, 3966 "blocked": { # this is an array of blocked currency 3967 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3968 }, 3969 "blockedGuarantee": { # this is locked money under collateral for futures 3970 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3971 }, 3972 }, 3973 } 3974 3975 # --- Prepare text table with limits in human-readable format: 3976 if show or onlyFiles: 3977 info = [ 3978 "# Withdrawal limits\n\n", 3979 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3980 "* **Account ID:** [{}]\n".format(self.accountId), 3981 ] 3982 3983 if view["limits"]["money"]: 3984 info.extend([ 3985 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3986 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3987 ]) 3988 3989 else: 3990 info.append("\nNo withdrawal limits\n") 3991 3992 for curr in view["limits"]["money"].keys(): 3993 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3994 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3995 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3996 3997 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3998 "[{}]".format(curr), 3999 "{:.2f}".format(view["limits"]["money"][curr]), 4000 "{:.2f}".format(availableMoney), 4001 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4002 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4003 ) 4004 4005 if curr == "rub": 4006 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4007 4008 else: 4009 info.append(infoStr) 4010 4011 infoText = "".join(info) 4012 4013 if show and not onlyFiles: 4014 uLogger.info(infoText) 4015 4016 if self.withdrawalLimitsFile and (show or onlyFiles): 4017 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4018 fH.write(infoText) 4019 4020 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4021 4022 if self.useHTMLReports: 4023 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4024 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4025 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4026 4027 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4028 4029 return view 4030 4031 def RequestAccounts(self) -> dict: 4032 """ 4033 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4034 4035 See also: 4036 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4037 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4038 - `OverviewUserInfo()` method 4039 4040 :return: dict with raw data from server that contains accounts info. Example of dict: 4041 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4042 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4043 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4044 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4045 """ 4046 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4047 4048 self.body = str({}) 4049 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4050 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4051 4052 if self.moreDebug: 4053 uLogger.debug("Records about available accounts successfully received") 4054 4055 return rawAccounts 4056 4057 def RequestUserInfo(self) -> dict: 4058 """ 4059 Method for requesting common user's information. 4060 4061 See also: 4062 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4063 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4064 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4065 - `OverviewUserInfo()` method 4066 4067 :return: dict with raw data from server that contains user's information. Example of dict: 4068 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4069 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4070 """ 4071 uLogger.debug("Requesting common user's information. Wait, please...") 4072 4073 self.body = str({}) 4074 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4075 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4076 4077 if self.moreDebug: 4078 uLogger.debug("Records about current user successfully received") 4079 4080 return rawUserInfo 4081 4082 def RequestMarginStatus(self, accountId: str = None) -> dict: 4083 """ 4084 Method for requesting margin calculation for defined account ID. 4085 4086 See also: 4087 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4088 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4089 - `OverviewUserInfo()` method 4090 4091 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4092 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4093 Example of responses: 4094 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4095 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4096 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4097 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4098 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4099 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4100 """ 4101 if accountId is None or not accountId: 4102 if self.accountId is None or not self.accountId: 4103 uLogger.error("Variable `accountId` must be defined for using this method!") 4104 raise Exception("Account ID required") 4105 4106 else: 4107 accountId = self.accountId # use `self.accountId` (main ID) by default 4108 4109 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4110 4111 self.body = str({"accountId": accountId}) 4112 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4113 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4114 4115 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4116 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4117 rawMargin = {} 4118 4119 else: 4120 if self.moreDebug: 4121 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4122 4123 return rawMargin 4124 4125 def RequestTariffLimits(self) -> dict: 4126 """ 4127 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4128 4129 See also: 4130 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4131 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4132 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4133 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4134 - `OverviewUserInfo()` method 4135 4136 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4137 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4138 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4139 """ 4140 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4141 4142 self.body = str({}) 4143 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4144 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4145 4146 if self.moreDebug: 4147 uLogger.debug("Records with limits of current tariff successfully received") 4148 4149 return rawTariffLimits 4150 4151 def RequestBondCoupons(self, iJSON: dict) -> dict: 4152 """ 4153 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4154 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4155 All dates are in UTC timezone. 4156 4157 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4158 Documentation: 4159 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4160 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4161 4162 See also: `ExtendBondsData()`. 4163 4164 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4165 If raw iJSON is not data of bond then server returns an error [400] with message: 4166 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4167 :return: dictionary with bond payment calendar. Response example 4168 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4169 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4170 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4171 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4172 """ 4173 if iJSON["figi"] is None or not iJSON["figi"]: 4174 uLogger.error("FIGI must be defined for using this method!") 4175 raise Exception("FIGI required") 4176 4177 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4178 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4179 4180 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4181 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4182 self._figi, 4183 startDate, 4184 endDate, 4185 )) 4186 4187 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4188 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4189 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4190 4191 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4192 uLogger.warning("Instrument type is not bond!") 4193 4194 else: 4195 if self.moreDebug: 4196 uLogger.debug("Records about bond payment calendar successfully received") 4197 4198 return calendar 4199 4200 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4201 """ 4202 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4203 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4204 coupon yields, current yields and some statistics etc. 4205 4206 WARNING! This is too long operation if a lot of bonds requested from broker server. 4207 4208 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4209 4210 :param instruments: list of strings with tickers or FIGIs. 4211 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4212 for further used by data scientists or stock analytics. 4213 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4214 In XLSX-file and Pandas DataFrame fields mean: 4215 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4216 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4217 """ 4218 if instruments is None or not instruments: 4219 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4220 raise Exception("Ticker or FIGI required") 4221 4222 if isinstance(instruments, str): 4223 instruments = [instruments] 4224 4225 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4226 4227 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4228 4229 iCount = len(uniqueInstruments) 4230 tooLong = iCount >= 20 4231 if tooLong: 4232 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4233 4234 bonds = None 4235 for i, self._figi in enumerate(uniqueInstruments): 4236 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4237 4238 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4239 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4240 rawBond = self.SearchByFIGI(requestPrice=True) 4241 4242 # Widen raw data with UTC current time (iData["actualDateTime"]): 4243 actualDate = datetime.now(tzutc()) 4244 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4245 4246 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4247 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4248 4249 # Replace some values with human-readable: 4250 iData["nominalCurrency"] = iData["nominal"]["currency"] 4251 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4252 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4253 iData["aciCurrency"] = iData["aciValue"]["currency"] 4254 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4255 iData["issueSize"] = int(iData["issueSize"]) 4256 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4257 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4258 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4259 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4260 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4261 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4262 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4263 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4264 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4265 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4266 4267 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4268 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4269 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4270 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4271 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4272 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4273 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4274 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4275 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4276 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4277 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4278 4279 # Widen raw data with calendar data from `rawCalendar` values: 4280 calendarData = [] 4281 if "events" in iData["rawCalendar"].keys(): 4282 for item in iData["rawCalendar"]["events"]: 4283 calendarData.append({ 4284 "couponDate": item["couponDate"], 4285 "couponNumber": int(item["couponNumber"]), 4286 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4287 "payCurrency": item["payOneBond"]["currency"], 4288 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4289 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4290 "couponStartDate": item["couponStartDate"], 4291 "couponEndDate": item["couponEndDate"], 4292 "couponPeriod": item["couponPeriod"], 4293 }) 4294 4295 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4296 if "maturityDate" not in iData.keys(): 4297 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4298 4299 # Widen raw data with Coupon Rate. 4300 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4301 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4302 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4303 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4304 4305 # Widen raw data with Yield to Maturity (YTM) on current date. 4306 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4307 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4308 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4309 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4310 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4311 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4312 4313 iData["calendar"] = calendarData # adds calendar at the end 4314 4315 # Remove not used data: 4316 iData.pop("uid") 4317 iData.pop("positionUid") 4318 iData.pop("currentPrice") 4319 iData.pop("rawCalendar") 4320 4321 colNames = list(iData.keys()) 4322 if bonds is None: 4323 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4324 4325 else: 4326 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4327 4328 else: 4329 uLogger.warning("Instrument is not a bond!") 4330 4331 processed = round(100 * (i + 1) / iCount, 1) 4332 if tooLong and processed % 5 == 0: 4333 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4334 4335 else: 4336 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4337 4338 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4339 4340 # Saving bonds from Pandas DataFrame to XLSX sheet: 4341 if xlsx and self.bondsXLSXFile: 4342 with pd.ExcelWriter( 4343 path=self.bondsXLSXFile, 4344 date_format=TKS_DATE_FORMAT, 4345 datetime_format=TKS_DATE_TIME_FORMAT, 4346 mode="w", 4347 ) as writer: 4348 bonds.to_excel( 4349 writer, 4350 sheet_name="Extended bonds data", 4351 index=True, 4352 encoding="UTF-8", 4353 freeze_panes=(1, 1), 4354 ) # saving as XLSX-file with freeze first row and column as headers 4355 4356 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4357 4358 return bonds 4359 4360 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4361 """ 4362 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4363 4364 WARNING! This is too long operation if a lot of bonds requested from broker server. 4365 4366 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4367 4368 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4369 extended information about bonds: main info, current prices, bond payment calendar, 4370 coupon yields, current yields and some statistics etc. 4371 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4372 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4373 for further used by data scientists or stock analytics. 4374 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4375 """ 4376 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4377 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4378 4379 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4380 4381 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4382 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4383 calendar = None 4384 for bond in extBonds.iterrows(): 4385 for item in bond[1]["calendar"]: 4386 cData = { 4387 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4388 "couponDate": item["couponDate"], 4389 "figi": bond[1]["figi"], 4390 "ticker": bond[1]["ticker"], 4391 "name": bond[1]["name"], 4392 "couponNumber": item["couponNumber"], 4393 "payOneBond": item["payOneBond"], 4394 "payCurrency": item["payCurrency"], 4395 "couponType": item["couponType"], 4396 "couponPeriod": item["couponPeriod"], 4397 "fixDate": item["fixDate"], 4398 "couponStartDate": item["couponStartDate"], 4399 "couponEndDate": item["couponEndDate"], 4400 } 4401 4402 if calendar is None: 4403 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4404 4405 else: 4406 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4407 4408 if calendar is not None: 4409 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4410 4411 # Saving calendar from Pandas DataFrame to XLSX sheet: 4412 if xlsx: 4413 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4414 4415 with pd.ExcelWriter( 4416 path=xlsxCalendarFile, 4417 date_format=TKS_DATE_FORMAT, 4418 datetime_format=TKS_DATE_TIME_FORMAT, 4419 mode="w", 4420 ) as writer: 4421 humanReadable = calendar.copy(deep=True) 4422 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4423 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4424 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4425 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable.columns = colNames # human-readable column names 4427 4428 humanReadable.to_excel( 4429 writer, 4430 sheet_name="Bond payments calendar", 4431 index=False, 4432 encoding="UTF-8", 4433 freeze_panes=(1, 2), 4434 ) # saving as XLSX-file with freeze first row and column as headers 4435 4436 del humanReadable # release df in memory 4437 4438 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4439 4440 return calendar 4441 4442 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4443 """ 4444 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4445 Also, creates Markdown file with calendar data, `calendar.md` by default. 4446 4447 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4448 4449 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4450 extended information about bonds: main info, current prices, bond payment calendar, 4451 coupon yields, current yields and some statistics etc. 4452 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4453 :param show: if `True` then also printing bonds payment calendar to the console, 4454 otherwise save to file `calendarFile` only. `False` by default. 4455 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4456 :return: multilines text in Markdown format with bonds payment calendar as a table. 4457 """ 4458 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4459 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4460 4461 infoText = "# Bond payments calendar\n\n" 4462 4463 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4464 4465 if not (calendar is None or calendar.empty): 4466 splitLine = "| | | | | | | | | |\n" 4467 4468 info = [ 4469 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4470 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4471 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4472 ] 4473 4474 newMonth = False 4475 notOneBond = calendar["figi"].nunique() > 1 4476 for i, bond in enumerate(calendar.iterrows()): 4477 if newMonth and notOneBond: 4478 info.append(splitLine) 4479 4480 info.append( 4481 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4482 " √" if bond[1]["paid"] else " —", 4483 bond[1]["couponDate"].split("T")[0], 4484 bond[1]["figi"], 4485 bond[1]["ticker"], 4486 bond[1]["couponNumber"], 4487 "{} {}".format( 4488 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4489 bond[1]["payCurrency"], 4490 ), 4491 bond[1]["couponType"], 4492 bond[1]["couponPeriod"], 4493 bond[1]["fixDate"].split("T")[0], 4494 ) 4495 ) 4496 4497 if i < len(calendar.values) - 1: 4498 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4499 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4500 newMonth = False if curDate.month == nextDate.month else True 4501 4502 else: 4503 newMonth = False 4504 4505 infoText += "".join(info) 4506 4507 if show and not onlyFiles: 4508 uLogger.info("{}".format(infoText)) 4509 4510 if self.calendarFile is not None and (show or onlyFiles): 4511 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4512 fH.write(infoText) 4513 4514 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4515 4516 if self.useHTMLReports: 4517 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4518 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4519 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4520 4521 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4522 4523 else: 4524 infoText += "No data\n" 4525 4526 return infoText 4527 4528 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4529 """ 4530 Method for parsing and show simple table with all available user accounts. 4531 4532 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4533 4534 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4535 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4536 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4537 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4538 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4539 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4540 "closed": "—", "access": "Full access" }, ...}}` 4541 """ 4542 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4543 4544 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4545 accounts = { 4546 item["id"]: { 4547 "type": TKS_ACCOUNT_TYPES[item["type"]], 4548 "name": item["name"], 4549 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4550 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4551 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4552 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4553 } for item in rawAccounts["accounts"] 4554 } 4555 4556 # Raw and parsed data with some fields replaced in "stat" section: 4557 view = { 4558 "rawAccounts": rawAccounts, 4559 "stat": accounts, 4560 } 4561 4562 # --- Prepare simple text table with only accounts data in human-readable format: 4563 if show or onlyFiles: 4564 info = [ 4565 "# User accounts\n\n", 4566 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4567 "| Account ID | Type | Status | Name |\n", 4568 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4569 ] 4570 4571 for account in view["stat"].keys(): 4572 info.extend([ 4573 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4574 account, 4575 view["stat"][account]["type"], 4576 view["stat"][account]["status"], 4577 view["stat"][account]["name"], 4578 ) 4579 ]) 4580 4581 infoText = "".join(info) 4582 4583 if show and not onlyFiles: 4584 uLogger.info(infoText) 4585 4586 if self.userAccountsFile and (show or onlyFiles): 4587 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4588 fH.write(infoText) 4589 4590 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4591 4592 if self.useHTMLReports: 4593 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4594 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4595 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4596 4597 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4598 4599 return view 4600 4601 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4602 """ 4603 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4604 4605 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4606 4607 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4608 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4609 :return: dict with raw parsed data from server and some calculated statistics about it. 4610 """ 4611 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4612 tmpTicker = self._ticker 4613 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4614 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4615 self._ticker = tmpTicker 4616 4617 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4618 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4619 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4620 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4621 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4622 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4623 4624 # This is dict with parsed common user data: 4625 userInfo = { 4626 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4627 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4628 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4629 "tariff": rawUserInfo["tariff"], 4630 } 4631 4632 # This is an array of dict with parsed margin statuses for every account IDs: 4633 margins = {} 4634 for accountId in accounts.keys(): 4635 if rawMargins[accountId]: 4636 margins[accountId] = { 4637 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4638 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4639 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4640 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4641 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4642 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4643 "missing": missing["volume"], 4644 } 4645 4646 else: 4647 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4648 4649 unary = {} # unary-connection limits 4650 for item in rawTariffLimits["unaryLimits"]: 4651 if item["limitPerMinute"] in unary.keys(): 4652 unary[item["limitPerMinute"]].extend(item["methods"]) 4653 4654 else: 4655 unary[item["limitPerMinute"]] = item["methods"] 4656 4657 stream = {} # stream-connection limits 4658 for item in rawTariffLimits["streamLimits"]: 4659 if item["limit"] in stream.keys(): 4660 stream[item["limit"]].extend(item["streams"]) 4661 4662 else: 4663 stream[item["limit"]] = item["streams"] 4664 4665 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4666 limits = { 4667 "unary": unary, 4668 "stream": stream, 4669 } 4670 4671 # Raw and parsed data as an output result: 4672 view = { 4673 "rawUserInfo": rawUserInfo, 4674 "rawAccounts": rawAccounts, 4675 "rawMargins": rawMargins, 4676 "rawTariffLimits": rawTariffLimits, 4677 "stat": { 4678 "overview": overview, 4679 "userInfo": userInfo, 4680 "accounts": accounts, 4681 "margins": margins, 4682 "limits": limits, 4683 }, 4684 } 4685 4686 # --- Prepare text table with user information in human-readable format: 4687 if show or onlyFiles: 4688 info = [ 4689 "# Full user information\n\n", 4690 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4691 "## Common information\n\n", 4692 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4693 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4694 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4695 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4696 "\n## User accounts\n\n", 4697 ] 4698 4699 for account in view["stat"]["accounts"].keys(): 4700 info.extend([ 4701 "### ID: [{}]\n\n".format(account), 4702 "| Parameters | Values |\n", 4703 "|----------------------|--------------------------------------------------------------|\n", 4704 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4705 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4706 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4707 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4708 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4709 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4710 ]) 4711 4712 if margins[account]: 4713 info.extend([ 4714 "| Margin status: | Enabled |\n", 4715 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4716 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4717 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4718 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4719 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4720 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4721 ]) 4722 4723 else: 4724 info.append("| Margin status: | Disabled |\n\n") 4725 4726 info.extend([ 4727 "\n## Current user tariff limits\n", 4728 "\n### See also\n", 4729 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4730 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4731 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4732 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4733 "\n### Unary limits\n", 4734 ]) 4735 4736 if unary: 4737 for key, values in sorted(unary.items()): 4738 info.append("\n* Max requests per minute: {}\n".format(key)) 4739 4740 for value in values: 4741 info.append(" - {}\n".format(value)) 4742 4743 else: 4744 info.append("\nNot available\n") 4745 4746 info.append("\n### Stream limits\n") 4747 4748 if stream: 4749 for key, values in sorted(stream.items()): 4750 info.append("\n* Max stream connections: {}\n".format(key)) 4751 4752 for value in values: 4753 info.append(" - {}\n".format(value)) 4754 4755 else: 4756 info.append("\nNot available\n") 4757 4758 infoText = "".join(info) 4759 4760 if show and not onlyFiles: 4761 uLogger.info(infoText) 4762 4763 if self.userInfoFile and (show or onlyFiles): 4764 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4765 fH.write(infoText) 4766 4767 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4768 4769 if self.useHTMLReports: 4770 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4771 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4772 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4773 4774 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4775 4776 return view 4777 4778 4779class Args: 4780 """ 4781 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4782 """ 4783 def __init__(self, **kwargs): 4784 self.__dict__.update(kwargs) 4785 4786 def __getattr__(self, item): 4787 return None 4788 4789 4790def ParseArgs(): 4791 """This function get and parse command line keys.""" 4792 parser = ArgumentParser() # command-line string parser 4793 4794 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4795 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4796 4797 # --- options: 4798 4799 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4800 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4801 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4802 4803 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4804 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4805 4806 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4807 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4808 4809 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4810 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4811 4812 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4813 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4814 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4815 4816 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4817 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4818 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4819 4820 # --- commands: 4821 4822 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4823 4824 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4825 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4826 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4827 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4828 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4829 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4830 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4831 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4832 4833 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4834 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4835 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4836 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4837 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4838 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4839 4840 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4841 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4842 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4843 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4844 4845 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4846 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4847 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4848 4849 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4850 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4851 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4852 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4853 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4854 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4855 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4856 4857 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4858 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4859 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4860 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4861 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4862 4863 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4864 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4865 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4866 4867 cmdArgs = parser.parse_args() 4868 return cmdArgs 4869 4870 4871def Main(**kwargs): 4872 """ 4873 Main function for work with TKSBrokerAPI in the console. 4874 4875 See examples: 4876 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4877 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4878 """ 4879 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4880 4881 if args.debug_level: 4882 uLogger.level = 10 # always debug level by default 4883 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4884 4885 exitCode = 0 4886 start = datetime.now(tzutc()) 4887 uLogger.debug("=-" * 50) 4888 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4889 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4890 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4891 )) 4892 4893 # trying to calculate full current version: 4894 buildVersion = __version__ 4895 try: 4896 v = version("tksbrokerapi") 4897 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4898 4899 except Exception: 4900 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4901 4902 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4903 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4904 4905 try: 4906 if args.version: 4907 print("TKSBrokerAPI {}".format(buildVersion)) 4908 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4909 4910 else: 4911 # Init class for trading with Tinkoff Broker: 4912 trader = TinkoffBrokerServer( 4913 token=args.token, 4914 accountId=args.account_id, 4915 useCache=not args.no_cache, 4916 ) 4917 4918 if args.tag is not None: 4919 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4920 4921 # --- set some options: 4922 4923 if args.more: 4924 trader.moreDebug = True 4925 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4926 4927 if args.html: 4928 trader.useHTMLReports = True 4929 4930 if args.ticker: 4931 ticker = str(args.ticker).upper() # Tickers may be upper case only 4932 4933 if ticker in trader.aliasesKeys: 4934 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4935 4936 else: 4937 trader.ticker = ticker 4938 4939 if args.figi: 4940 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4941 4942 if args.depth is not None: 4943 trader.depth = args.depth 4944 4945 # --- do one command: 4946 4947 if args.list: 4948 if args.output is not None: 4949 trader.instrumentsFile = args.output 4950 4951 trader.ShowInstrumentsInfo(show=True) 4952 4953 elif args.list_xlsx: 4954 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4955 4956 elif args.bonds_xlsx is not None: 4957 if args.output is not None: 4958 trader.bondsXLSXFile = args.output 4959 4960 if len(args.bonds_xlsx) == 0: 4961 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4962 4963 else: 4964 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4965 4966 elif args.search: 4967 if args.output is not None: 4968 trader.searchResultsFile = args.output 4969 4970 trader.SearchInstruments(pattern=args.search[0], show=True) 4971 4972 elif args.info: 4973 if not (args.ticker or args.figi): 4974 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4975 raise Exception("Ticker or FIGI required") 4976 4977 if args.output is not None: 4978 trader.infoFile = args.output 4979 4980 if args.ticker: 4981 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4982 4983 else: 4984 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4985 4986 elif args.calendar is not None: 4987 if args.output is not None: 4988 trader.calendarFile = args.output 4989 4990 if len(args.calendar) == 0: 4991 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4992 4993 else: 4994 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4995 4996 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4997 4998 elif args.price: 4999 if not (args.ticker or args.figi): 5000 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5001 raise Exception("Ticker or FIGI required") 5002 5003 trader.GetCurrentPrices(show=True) 5004 5005 elif args.prices is not None: 5006 if args.output is not None: 5007 trader.pricesFile = args.output 5008 5009 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5010 5011 elif args.overview: 5012 if args.output is not None: 5013 trader.overviewFile = args.output 5014 5015 trader.Overview(show=True, details="full") 5016 5017 elif args.overview_digest: 5018 if args.output is not None: 5019 trader.overviewDigestFile = args.output 5020 5021 trader.Overview(show=True, details="digest") 5022 5023 elif args.overview_positions: 5024 if args.output is not None: 5025 trader.overviewPositionsFile = args.output 5026 5027 trader.Overview(show=True, details="positions") 5028 5029 elif args.overview_orders: 5030 if args.output is not None: 5031 trader.overviewOrdersFile = args.output 5032 5033 trader.Overview(show=True, details="orders") 5034 5035 elif args.overview_analytics: 5036 if args.output is not None: 5037 trader.overviewAnalyticsFile = args.output 5038 5039 trader.Overview(show=True, details="analytics") 5040 5041 elif args.overview_calendar: 5042 if args.output is not None: 5043 trader.overviewAnalyticsFile = args.output 5044 5045 trader.Overview(show=True, details="calendar") 5046 5047 elif args.deals is not None: 5048 if args.output is not None: 5049 trader.reportFile = args.output 5050 5051 if 0 <= len(args.deals) < 3: 5052 trader.Deals( 5053 start=args.deals[0] if len(args.deals) >= 1 else None, 5054 end=args.deals[1] if len(args.deals) == 2 else None, 5055 show=True, # Always show deals report in console 5056 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5057 ) 5058 5059 else: 5060 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5061 raise Exception("Incorrect value") 5062 5063 elif args.history is not None: 5064 if args.output is not None: 5065 trader.historyFile = args.output 5066 5067 if 0 <= len(args.history) < 3: 5068 dataReceived = trader.History( 5069 start=args.history[0] if len(args.history) >= 1 else None, 5070 end=args.history[1] if len(args.history) == 2 else None, 5071 interval="hour" if args.interval is None or not args.interval else args.interval, 5072 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5073 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5074 show=True, # shows all downloaded candles in console 5075 ) 5076 5077 if args.render_chart is not None and dataReceived is not None: 5078 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5079 5080 trader.ShowHistoryChart( 5081 candles=dataReceived, 5082 interact=iChart, 5083 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5084 ) 5085 5086 else: 5087 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5088 raise Exception("Incorrect value") 5089 5090 elif args.load_history is not None: 5091 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5092 5093 if args.render_chart is not None and histData is not None: 5094 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5095 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5096 5097 trader.ShowHistoryChart( 5098 candles=histData, 5099 interact=iChart, 5100 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5101 ) 5102 5103 elif args.trade is not None: 5104 if 1 <= len(args.trade) <= 5: 5105 trader.Trade( 5106 operation=args.trade[0], 5107 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5108 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5109 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5110 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5111 ) 5112 5113 else: 5114 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5115 5116 elif args.buy is not None: 5117 if 0 <= len(args.buy) <= 4: 5118 trader.Buy( 5119 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5120 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5121 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5122 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5123 ) 5124 5125 else: 5126 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5127 5128 elif args.sell is not None: 5129 if 0 <= len(args.sell) <= 4: 5130 trader.Sell( 5131 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5132 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5133 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5134 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5135 ) 5136 5137 else: 5138 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5139 5140 elif args.order: 5141 if 4 <= len(args.order) <= 7: 5142 trader.Order( 5143 operation=args.order[0], 5144 orderType=args.order[1], 5145 lots=int(args.order[2]), 5146 targetPrice=float(args.order[3]), 5147 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5148 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5149 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5150 ) 5151 5152 else: 5153 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5154 5155 elif args.buy_limit: 5156 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5157 5158 elif args.sell_limit: 5159 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5160 5161 elif args.buy_stop: 5162 if 2 <= len(args.buy_stop) <= 7: 5163 trader.BuyStop( 5164 lots=int(args.buy_stop[0]), 5165 targetPrice=float(args.buy_stop[1]), 5166 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5167 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5168 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5169 ) 5170 5171 else: 5172 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5173 5174 elif args.sell_stop: 5175 if 2 <= len(args.sell_stop) <= 7: 5176 trader.SellStop( 5177 lots=int(args.sell_stop[0]), 5178 targetPrice=float(args.sell_stop[1]), 5179 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5180 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5181 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5182 ) 5183 5184 else: 5185 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5186 5187 # elif args.buy_order_grid is not None: 5188 # # update order grid work with api v2 5189 # if len(args.buy_order_grid) == 2: 5190 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5191 # 5192 # for order in orderParams: 5193 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5194 # 5195 # else: 5196 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5197 # 5198 # elif args.sell_order_grid is not None: 5199 # # update order grid work with api v2 5200 # if len(args.sell_order_grid) >= 2: 5201 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5202 # 5203 # for order in orderParams: 5204 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5205 # 5206 # else: 5207 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5208 5209 elif args.close_order is not None: 5210 trader.CloseOrders(args.close_order) # close only one order 5211 5212 elif args.close_orders is not None: 5213 trader.CloseOrders(args.close_orders) # close list of orders 5214 5215 elif args.close_trade: 5216 if not (args.ticker or args.figi): 5217 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5218 raise Exception("Ticker or FIGI required") 5219 5220 if args.ticker: 5221 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5222 5223 else: 5224 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5225 5226 elif args.close_trades is not None: 5227 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5228 5229 elif args.close_all is not None: 5230 if args.ticker: 5231 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5232 5233 elif args.figi: 5234 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5235 5236 else: 5237 trader.CloseAll(*args.close_all) 5238 5239 elif args.limits: 5240 if args.output is not None: 5241 trader.withdrawalLimitsFile = args.output 5242 5243 trader.OverviewLimits(show=True) 5244 5245 elif args.user_info: 5246 if args.output is not None: 5247 trader.userInfoFile = args.output 5248 5249 trader.OverviewUserInfo(show=True) 5250 5251 elif args.account: 5252 if args.output is not None: 5253 trader.userAccountsFile = args.output 5254 5255 trader.OverviewAccounts(show=True) 5256 5257 else: 5258 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5259 raise Exception("There is no command to execute") 5260 5261 except Exception: 5262 trace = tb.format_exc() 5263 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5264 if e in trace: 5265 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5266 break 5267 5268 uLogger.debug(trace) 5269 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5270 exitCode = 255 # an error occurred, must be open a ticket for this issue 5271 5272 finally: 5273 finish = datetime.now(tzutc()) 5274 5275 if exitCode == 0: 5276 if args.more: 5277 uLogger.debug("All operations were finished success (summary code is 0).") 5278 5279 else: 5280 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5281 os.path.abspath(uLog.defaultLogFile), exitCode, 5282 )) 5283 5284 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5285 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5286 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5287 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5288 )) 5289 uLogger.debug("=-" * 50) 5290 5291 if not kwargs: 5292 sys.exit(exitCode) 5293 5294 else: 5295 return exitCode 5296 5297 5298if __name__ == "__main__": 5299 Main()
78class TinkoffBrokerServer: 79 """ 80 This class implements methods to work with Tinkoff broker server. 81 82 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 83 84 About `token`: https://tinkoff.github.io/investAPI/token/ 85 """ 86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self._tag = "" 130 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 131 132 self.__lock = Lock() # initialize multiprocessing mutex lock 133 134 self.aliases = TKS_TICKER_ALIASES 135 """Some aliases instead official tickers. 136 137 See also: `TKSEnums.TKS_TICKER_ALIASES` 138 """ 139 140 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 141 142 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 143 144 self._ticker = "" 145 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 146 147 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 148 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 149 150 See also: `SearchByTicker()`, `SearchInstruments()`. 151 """ 152 153 self._figi = "" 154 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 155 156 See also: `SearchByFIGI()`, `SearchInstruments()`. 157 """ 158 159 self.depth = 1 160 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 161 162 See also: `GetCurrentPrices()`. 163 """ 164 165 self.server = r"https://invest-public-api.tinkoff.ru/rest" 166 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 167 168 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 169 """ 170 171 uLogger.debug("Broker API server: {}".format(self.server)) 172 173 self.timeout = 15 174 """Server operations timeout in seconds. Default: `15`. 175 176 See also: `SendAPIRequest()`. 177 """ 178 179 self.headers = { 180 "Content-Type": "application/json", 181 "accept": "application/json", 182 "Authorization": "Bearer {}".format(self.token), 183 "x-app-name": "Tim55667757.TKSBrokerAPI", 184 } 185 """ 186 Headers which send in every request to broker server. Please, do not change it! 187 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.body = None 193 """Request body which send to broker server. Default: `None`. 194 195 See also: `SendAPIRequest()`. 196 """ 197 198 self.moreDebug = False 199 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 200 201 self.useHTMLReports = False 202 """ 203 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 204 205 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 206 """ 207 208 self.historyFile = None 209 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 210 211 See also: `History()`. 212 """ 213 214 self.htmlHistoryFile = "index.html" 215 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 216 217 See also: `ShowHistoryChart()`. 218 """ 219 220 self.instrumentsFile = "instruments.md" 221 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 222 223 See also: `ShowInstrumentsInfo()`. 224 """ 225 226 self.searchResultsFile = "search-results.md" 227 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 228 229 See also: `SearchInstruments()`. 230 """ 231 232 self.pricesFile = "prices.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `GetListOfPrices()`. 236 """ 237 238 self.infoFile = "info.md" 239 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 240 241 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 242 """ 243 244 self.bondsXLSXFile = "ext-bonds.xlsx" 245 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 246 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 247 248 See also: `ExtendBondsData()`. 249 """ 250 251 self.calendarFile = "calendar.md" 252 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 253 254 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 255 256 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 257 """ 258 259 self.overviewFile = "overview.md" 260 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 261 262 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 263 """ 264 265 self.overviewDigestFile = "overview-digest.md" 266 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 267 268 See also: `Overview()` with parameter `details="digest"`. 269 """ 270 271 self.overviewPositionsFile = "overview-positions.md" 272 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 273 274 See also: `Overview()` with parameter `details="positions"`. 275 """ 276 277 self.overviewOrdersFile = "overview-orders.md" 278 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 279 280 See also: `Overview()` with parameter `details="orders"`. 281 """ 282 283 self.overviewAnalyticsFile = "overview-analytics.md" 284 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 285 286 See also: `Overview()` with parameter `details="analytics"`. 287 """ 288 289 self.overviewBondsCalendarFile = "overview-calendar.md" 290 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 291 292 See also: `Overview()` with parameter `details="calendar"`. 293 """ 294 295 self.reportFile = "deals.md" 296 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 297 298 See also: `Deals()`. 299 """ 300 301 self.withdrawalLimitsFile = "limits.md" 302 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 303 304 See also: `OverviewLimits()` and `RequestLimits()`. 305 """ 306 307 self.userInfoFile = "user-info.md" 308 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 309 310 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 311 """ 312 313 self.userAccountsFile = "accounts.md" 314 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 315 316 See also: `OverviewAccounts()`, `RequestAccounts()`. 317 """ 318 319 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 320 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 321 322 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 323 324 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 325 """ 326 327 self.iList = None # init iList for raw instruments data 328 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 329 330 See also: `Listing()`, `DumpInstruments()`. 331 """ 332 333 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 334 if useCache: 335 if os.path.exists(self.iListDumpFile): 336 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 337 curTime = datetime.now(tzutc()) 338 339 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 340 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 341 342 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 343 344 else: 345 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 346 347 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 348 os.path.abspath(self.iListDumpFile), 349 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 350 )) 351 352 else: 353 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 354 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 355 356 else: 357 self.iList = self.Listing() # request new raw instruments data from broker server 358 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 359 360 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 361 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 362 363 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 364 """ 365 366 @property 367 def tag(self) -> str: 368 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 369 return self._tag 370 371 @tag.setter 372 def tag(self, value): 373 """Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 374 self._tag = str(value) 375 376 if self._tag: 377 for handler in uLogger.handlers: 378 handler.setFormatter(uLog.logging.Formatter(uLog.formatStringWithTag.format(tag=self._tag))) 379 380 uLogger.debug("Custom TKSBrokerAPI tag was set: {}".format(self._tag)) 381 382 else: 383 for handler in uLogger.handlers: 384 handler.setFormatter(uLog.logging.Formatter(uLog.formatString)) 385 386 uLogger.debug("Default logger format is used") 387 388 @property 389 def ticker(self) -> str: 390 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 391 392 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 393 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 394 395 See also: `SearchByTicker()`, `SearchInstruments()`. 396 """ 397 return self._ticker 398 399 @ticker.setter 400 def ticker(self, value): 401 """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only. 402 403 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 404 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 405 406 See also: `SearchByTicker()`, `SearchInstruments()`. 407 """ 408 self._ticker = str(value).upper() # Tickers may be upper case only 409 410 @property 411 def figi(self) -> str: 412 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 413 414 See also: `SearchByFIGI()`, `SearchInstruments()`. 415 """ 416 return self._figi 417 418 @figi.setter 419 def figi(self, value): 420 """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 421 422 See also: `SearchByFIGI()`, `SearchInstruments()`. 423 """ 424 self._figi = str(value).upper() # FIGI may be upper case only 425 426 def _ParseJSON(self, rawData="{}") -> dict: 427 """ 428 Parse JSON from response string. 429 430 :param rawData: this is a string with JSON-formatted text. 431 :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`. 432 """ 433 try: 434 responseJSON = json.loads(rawData) if rawData else {} 435 436 if self.moreDebug: 437 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 438 439 return responseJSON 440 441 except Exception as e: 442 uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e)) 443 444 return {} 445 446 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 447 """ 448 Send GET or POST request to broker server and receive JSON object. 449 450 self.header: must be defining with dictionary of headers. 451 self.body: if define then used as request body. None by default. 452 self.timeout: global request timeout, 15 seconds by default. 453 :param url: url with REST request. 454 :param reqType: send "GET" or "POST" request. "GET" by default. 455 :param retry: how many times retry after first request if an 5xx server errors occurred. 456 :param pause: sleep time in seconds between retries. 457 :return: response JSON (dictionary) from broker. 458 """ 459 if reqType.upper() not in ("GET", "POST"): 460 uLogger.error("You can define request type: `GET` or `POST`!") 461 raise Exception("Incorrect value") 462 463 if self.moreDebug: 464 uLogger.debug("Request parameters:") 465 uLogger.debug(" - REST API URL: {}".format(url)) 466 uLogger.debug(" - request type: {}".format(reqType)) 467 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 468 uLogger.debug(" - body:\n{}".format(self.body)) 469 470 # fast hack to avoid all operations with some tickers/FIGI 471 responseJSON = {} 472 oK = True 473 for item in self.exclude: 474 if item in url: 475 if self.moreDebug: 476 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 477 478 oK = False 479 break 480 481 if oK: 482 with self.__lock: # acquire the mutex lock 483 counter = 0 484 response = None 485 errMsg = "" 486 487 while not response and counter <= retry: 488 if reqType == "GET": 489 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 490 491 if reqType == "POST": 492 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 493 494 if self.moreDebug: 495 uLogger.debug("Response:") 496 uLogger.debug(" - status code: {}".format(response.status_code)) 497 uLogger.debug(" - reason: {}".format(response.reason)) 498 uLogger.debug(" - body length: {}".format(len(response.text))) 499 uLogger.debug(" - headers:\n{}".format(response.headers)) 500 501 # Server returns some headers: 502 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 503 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 504 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 505 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 506 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 507 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 508 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 509 sleep(rateLimitWait) 510 511 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 512 if 400 <= response.status_code < 500: 513 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 514 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 515 516 if "code" in response.text and "message" in response.text: 517 msgDict = self._ParseJSON(rawData=response.text) 518 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 519 520 counter = retry + 1 # do not retry for 4xx errors 521 522 if 500 <= response.status_code < 600: 523 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 524 uLogger.debug(" - not oK, {}".format(errMsg)) 525 526 if "code" in response.text and "message" in response.text: 527 errMsgDict = self._ParseJSON(rawData=response.text) 528 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 529 530 counter += 1 531 532 if counter <= retry: 533 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 534 sleep(pause) 535 536 responseJSON = self._ParseJSON(rawData=response.text) 537 538 if errMsg: 539 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 540 uLogger.error(" - not oK, {}".format(errMsg)) 541 542 return responseJSON 543 544 def _IUpdater(self, iType: str) -> tuple: 545 """ 546 Request instrument by type from server. See available API methods for instruments: 547 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 548 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 549 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 550 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 551 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 552 553 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 554 :return: tuple with iType name and list of available instruments of current type for defined user token. 555 """ 556 result = [] 557 558 if iType in TKS_INSTRUMENTS: 559 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 560 561 # all instruments have the same body in API v2 requests: 562 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 563 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 564 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 565 566 return iType, result 567 568 def _IWrapper(self, kwargs): 569 """ 570 Wrapper runs instrument's update method `_IUpdater()`. 571 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 572 """ 573 return self._IUpdater(**kwargs) 574 575 def Listing(self) -> dict: 576 """ 577 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 578 579 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 580 """ 581 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 582 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 583 584 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 585 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 586 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 587 588 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 589 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 590 poolUpdater.close() # close the thread pool 591 poolUpdater.join() # wait a moment until all data returns from threads 592 593 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 594 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 595 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 596 597 # calculate minimum price increment (step) for all instruments and set up instrument's type: 598 for iType in iList.keys(): 599 for ticker in iList[iType]: 600 iList[iType][ticker]["type"] = iType 601 602 if "minPriceIncrement" in iList[iType][ticker].keys(): 603 iList[iType][ticker]["step"] = NanoToFloat( 604 iList[iType][ticker]["minPriceIncrement"]["units"], 605 iList[iType][ticker]["minPriceIncrement"]["nano"], 606 ) 607 608 else: 609 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 610 611 return iList 612 613 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 614 """ 615 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 616 617 See also: `DumpInstruments()`, `Listing()`. 618 619 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 620 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 621 """ 622 if self.iListDumpFile is None or not self.iListDumpFile: 623 uLogger.error("Output name of dump file must be defined!") 624 raise Exception("Filename required") 625 626 if not self.iList or forceUpdate: 627 self.iList = self.Listing() 628 629 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 630 631 # Save as XLSX with separated sheets for every type of instruments: 632 with pd.ExcelWriter( 633 path=xlsxDumpFile, 634 date_format=TKS_DATE_FORMAT, 635 datetime_format=TKS_DATE_TIME_FORMAT, 636 mode="w", 637 ) as writer: 638 for iType in TKS_INSTRUMENTS: 639 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 640 df = df[sorted(df)] # sorted by column names 641 df = df.applymap( 642 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 643 na_action="ignore", 644 ) # converting numbers from nano-type to float in every cell 645 df.to_excel( 646 writer, 647 sheet_name=iType, 648 encoding="UTF-8", 649 freeze_panes=(1, 1), 650 ) # saving as XLSX-file with freeze first row and column as headers 651 652 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 653 654 def DumpInstruments(self, forceUpdate: bool = True) -> str: 655 """ 656 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 657 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 658 659 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 660 661 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 662 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 663 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 664 """ 665 if self.iListDumpFile is None or not self.iListDumpFile: 666 uLogger.error("Output name of dump file must be defined!") 667 raise Exception("Filename required") 668 669 if not self.iList or forceUpdate: 670 self.iList = self.Listing() 671 672 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 673 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 674 fH.write(jsonDump) 675 676 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 677 678 return jsonDump 679 680 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 681 """ 682 Show information about one instrument defined by json data and prints it in Markdown format. 683 684 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 685 686 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 687 :param show: if `True` then also printing information about instrument and its current price. 688 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 689 :return: multilines text in Markdown format with information about one instrument. 690 """ 691 splitLine = "| | |\n" 692 infoText = "" 693 694 if iJSON is not None and iJSON and isinstance(iJSON, dict): 695 info = [ 696 "# Main information\n\n", 697 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 698 "| Parameters | Values |\n", 699 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 700 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 701 "| Full name: | {:<54} |\n".format(iJSON["name"]), 702 ] 703 704 if "sector" in iJSON.keys() and iJSON["sector"]: 705 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 706 707 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 708 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 709 710 info.extend([ 711 splitLine, 712 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 713 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 714 ]) 715 716 if "isin" in iJSON.keys() and iJSON["isin"]: 717 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 718 719 if "classCode" in iJSON.keys(): 720 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 721 722 info.extend([ 723 splitLine, 724 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 725 splitLine, 726 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 727 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 728 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 729 ]) 730 731 if iJSON["figi"]: 732 self._figi = iJSON["figi"] 733 iJSON = iJSON | self.RequestTradingStatus() 734 735 info.extend([ 736 splitLine, 737 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 738 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 739 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 740 ]) 741 742 info.append(splitLine) 743 744 if "type" in iJSON.keys() and iJSON["type"]: 745 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 746 747 if "shareType" in iJSON.keys() and iJSON["shareType"]: 748 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 749 750 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 751 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 752 753 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 754 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 755 756 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 757 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 758 759 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 760 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 761 762 if "focusType" in iJSON.keys() and iJSON["focusType"]: 763 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 764 765 if "assetType" in iJSON.keys() and iJSON["assetType"]: 766 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 767 768 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 769 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 770 771 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 772 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 773 774 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 775 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 776 777 if "currency" in iJSON.keys(): 778 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 779 780 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 781 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 782 783 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 784 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 785 786 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 787 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 788 789 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 790 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 791 792 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 793 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 794 795 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 796 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 797 798 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 799 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 800 801 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 802 info.append("| Perpetual bond: | Yes |\n") 803 804 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 805 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 806 807 iExt = None 808 if iJSON["type"] == "Bonds": 809 info.extend([ 810 splitLine, 811 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 812 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 813 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 814 iJSON["nominal"]["currency"], 815 )), 816 ]) 817 818 if "floatingCouponFlag" in iJSON.keys(): 819 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 820 821 if "amortizationFlag" in iJSON.keys(): 822 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 823 824 info.append(splitLine) 825 826 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 827 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 828 829 if iJSON["figi"]: 830 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 831 832 info.extend([ 833 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 834 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 835 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 836 ]) 837 838 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 839 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 840 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 841 iJSON["aciValue"]["currency"] 842 ))) 843 844 if "currentPrice" in iJSON.keys(): 845 info.append(splitLine) 846 847 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 848 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 849 850 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 851 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 852 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 853 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 854 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 855 856 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 857 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 858 859 info.extend([ 860 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 861 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 862 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 863 )), 864 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 865 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 866 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 867 )), 868 "| Changes between last deal price and last close | {:<54} |\n".format( 869 "{:.2f}%{}".format( 870 iJSON["currentPrice"]["changes"], 871 " ({}{:.2f} {})".format( 872 "+" if bondChangesDelta > 0 else "", 873 bondChangesDelta, 874 aciCurrency 875 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 876 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 877 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 878 currency 879 ), 880 ) 881 ), 882 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 883 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 884 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 885 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 886 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 887 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 888 )), 889 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 890 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 891 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 892 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 893 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 894 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 895 )), 896 ]) 897 898 if "lot" in iJSON.keys(): 899 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 900 901 if "step" in iJSON.keys() and iJSON["step"] != 0: 902 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 903 904 # Add bond payment calendar: 905 if iJSON["type"] == "Bonds": 906 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 907 info.extend(["\n#", strCalendar]) 908 909 infoText += "".join(info) 910 911 if show and not onlyFiles: 912 uLogger.info("{}".format(infoText)) 913 914 if self.infoFile is not None and (show or onlyFiles): 915 with open(self.infoFile, "w", encoding="UTF-8") as fH: 916 fH.write(infoText) 917 918 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 919 920 if self.useHTMLReports: 921 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 922 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 923 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 924 925 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 926 927 return infoText 928 929 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 930 """ 931 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 932 933 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 934 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 935 :return: JSON formatted data with information about instrument. 936 """ 937 tickerJSON = {} 938 if self.moreDebug: 939 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 940 941 if not self._ticker: 942 uLogger.warning("self._ticker variable is not be empty!") 943 944 else: 945 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 946 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 947 raise Exception("Instrument not allowed") 948 949 if not self.iList: 950 self.iList = self.Listing() 951 952 if self._ticker in self.iList["Shares"].keys(): 953 tickerJSON = self.iList["Shares"][self._ticker] 954 if self.moreDebug: 955 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 956 957 elif self._ticker in self.iList["Currencies"].keys(): 958 tickerJSON = self.iList["Currencies"][self._ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 961 962 elif self._ticker in self.iList["Bonds"].keys(): 963 tickerJSON = self.iList["Bonds"][self._ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 966 967 elif self._ticker in self.iList["Etfs"].keys(): 968 tickerJSON = self.iList["Etfs"][self._ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 971 972 elif self._ticker in self.iList["Futures"].keys(): 973 tickerJSON = self.iList["Futures"][self._ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 976 977 if tickerJSON: 978 self._figi = tickerJSON["figi"] 979 980 if requestPrice: 981 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 982 983 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 984 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 985 986 else: 987 tickerJSON["currentPrice"]["changes"] = 0 988 989 if show: 990 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 991 992 else: 993 if show: 994 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 995 996 return tickerJSON 997 998 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 999 """ 1000 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1001 1002 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1003 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1004 :return: JSON formatted data with information about instrument. 1005 """ 1006 figiJSON = {} 1007 if self.moreDebug: 1008 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1009 1010 if not self._figi: 1011 uLogger.warning("self._figi variable is not be empty!") 1012 1013 else: 1014 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1015 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1016 raise Exception("Instrument not allowed") 1017 1018 if not self.iList: 1019 self.iList = self.Listing() 1020 1021 for item in self.iList["Shares"].keys(): 1022 if self._figi == self.iList["Shares"][item]["figi"]: 1023 figiJSON = self.iList["Shares"][item] 1024 1025 if self.moreDebug: 1026 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1027 1028 break 1029 1030 if not figiJSON: 1031 for item in self.iList["Currencies"].keys(): 1032 if self._figi == self.iList["Currencies"][item]["figi"]: 1033 figiJSON = self.iList["Currencies"][item] 1034 1035 if self.moreDebug: 1036 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Bonds"].keys(): 1042 if self._figi == self.iList["Bonds"][item]["figi"]: 1043 figiJSON = self.iList["Bonds"][item] 1044 1045 if self.moreDebug: 1046 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Etfs"].keys(): 1052 if self._figi == self.iList["Etfs"][item]["figi"]: 1053 figiJSON = self.iList["Etfs"][item] 1054 1055 if self.moreDebug: 1056 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Futures"].keys(): 1062 if self._figi == self.iList["Futures"][item]["figi"]: 1063 figiJSON = self.iList["Futures"][item] 1064 1065 if self.moreDebug: 1066 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1067 1068 break 1069 1070 if figiJSON: 1071 self._figi = figiJSON["figi"] 1072 self._ticker = figiJSON["ticker"] 1073 1074 if requestPrice: 1075 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1076 1077 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1078 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1079 1080 else: 1081 figiJSON["currentPrice"]["changes"] = 0 1082 1083 if show: 1084 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1085 1086 else: 1087 if show: 1088 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1089 1090 return figiJSON 1091 1092 def GetCurrentPrices(self, show: bool = True) -> dict: 1093 """ 1094 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1095 `{"buy": [{"price": 1243.8, "quantity": 193}, 1096 {"price": 1244.0, "quantity": 168}, 1097 {"price": 1244.8, "quantity": 5}, 1098 {"price": 1245.0, "quantity": 61}, 1099 {"price": 1245.4, "quantity": 60}], 1100 "sell": [{"price": 1243.6, "quantity": 8}, 1101 {"price": 1242.6, "quantity": 10}, 1102 {"price": 1242.4, "quantity": 18}, 1103 {"price": 1242.2, "quantity": 50}, 1104 {"price": 1242.0, "quantity": 113}], 1105 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1106 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1107 - sell: list of dicts with Buyers prices, 1108 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1109 - quantity: volume value by current price in lots, 1110 - limitUp: current trade session limit price, maximum, 1111 - limitDown: current trade session limit price, minimum, 1112 - lastPrice: last deal price of the instrument, 1113 - closePrice: previous trade session close price of the instrument. 1114 1115 See also: `SearchByTicker()` and `SearchByFIGI()`. 1116 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1117 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1118 1119 :param show: if `True` then print DOM to log and console. 1120 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1121 If an error occurred then returns an empty record: 1122 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1123 """ 1124 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1125 1126 if self.depth < 1: 1127 uLogger.error("Depth of Market (DOM) must be >=1!") 1128 raise Exception("Incorrect value") 1129 1130 if not (self._ticker or self._figi): 1131 uLogger.error("self._ticker or self._figi variables must be defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 if self._ticker and not self._figi: 1135 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1136 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1137 1138 if not self._ticker and self._figi: 1139 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1141 1142 if not self._figi: 1143 uLogger.error("FIGI is not defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 else: 1147 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1148 1149 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1150 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1151 self.body = str({"figi": self._figi, "depth": self.depth}) 1152 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1153 1154 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1155 # list of dicts with sellers orders: 1156 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1157 1158 # list of dicts with buyers orders: 1159 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1160 1161 # max price of instrument at this time: 1162 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1163 1164 # min price of instrument at this time: 1165 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1166 1167 # last price of deal with instrument: 1168 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1169 1170 # last close price of instrument: 1171 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1172 1173 else: 1174 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1175 uLogger.debug("Server response: {}".format(pricesResponse)) 1176 1177 if show: 1178 if prices["buy"] or prices["sell"]: 1179 info = [ 1180 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1181 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1182 self._ticker, 1183 self._figi, 1184 self.depth, 1185 ), 1186 "-" * 60, "\n", 1187 " Orders of Buyers | Orders of Sellers\n", 1188 "-" * 60, "\n", 1189 " Sell prices (volumes) | Buy prices (volumes)\n", 1190 "-" * 60, "\n", 1191 ] 1192 1193 if not prices["buy"]: 1194 info.append(" | No orders!\n") 1195 sumBuy = 0 1196 1197 else: 1198 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1199 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1200 for item in maxMinSorted: 1201 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1202 1203 if not prices["sell"]: 1204 info.append("No orders! |\n") 1205 sumSell = 0 1206 1207 else: 1208 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1209 for item in prices["sell"]: 1210 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1211 1212 info.extend([ 1213 "-" * 60, "\n", 1214 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1215 "-" * 60, "\n", 1216 ]) 1217 1218 infoText = "".join(info) 1219 1220 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1221 1222 else: 1223 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1224 1225 return prices 1226 1227 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1228 """ 1229 This method get and show information about all available broker instruments for current user account. 1230 If `instrumentsFile` string is not empty then also save information to this file. 1231 1232 :param show: if `True` then print results to console, if `False` — print only to file. 1233 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1234 :return: multi-lines string with all available broker instruments. 1235 """ 1236 if not self.iList: 1237 self.iList = self.Listing() 1238 1239 info = [ 1240 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1241 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1242 ] 1243 1244 # add instruments count by type: 1245 for iType in self.iList.keys(): 1246 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1247 1248 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1249 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1250 1251 # generating info tables with all instruments by type: 1252 for iType in self.iList.keys(): 1253 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1254 1255 for instrument in self.iList[iType].keys(): 1256 iName = self.iList[iType][instrument]["name"] # instrument's name 1257 if len(iName) > 57: 1258 iName = "{}...".format(iName[:54]) # right trim for a long string 1259 1260 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1261 self.iList[iType][instrument]["ticker"], 1262 iName, 1263 self.iList[iType][instrument]["figi"], 1264 self.iList[iType][instrument]["currency"], 1265 self.iList[iType][instrument]["lot"], 1266 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1267 )) 1268 1269 infoText = "".join(info) 1270 1271 if show and not onlyFiles: 1272 uLogger.info(infoText) 1273 1274 if self.instrumentsFile and (show or onlyFiles): 1275 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1276 fH.write(infoText) 1277 1278 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1279 1280 if self.useHTMLReports: 1281 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1282 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1283 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1284 1285 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1286 1287 return infoText 1288 1289 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1290 """ 1291 This method search and show information about instruments by part of its ticker, FIGI or name. 1292 If `searchResultsFile` string is not empty then also save information to this file. 1293 1294 :param pattern: string with part of ticker, FIGI or instrument's name. 1295 :param show: if `True` then print results to console, if `False` — return list of result only. 1296 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1297 :return: list of dictionaries with all found instruments. 1298 """ 1299 if not self.iList: 1300 self.iList = self.Listing() 1301 1302 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1303 compiledPattern = re.compile(pattern, re.IGNORECASE) 1304 1305 for iType in self.iList: 1306 for instrument in self.iList[iType].values(): 1307 searchResult = compiledPattern.search(" ".join( 1308 [instrument["ticker"], instrument["figi"], instrument["name"]] 1309 )) 1310 1311 if searchResult: 1312 searchResults[iType][instrument["ticker"]] = instrument 1313 1314 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1315 info = [ 1316 "# Search results\n\n", 1317 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1318 "* **Search pattern:** [{}]\n".format(pattern), 1319 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1320 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1321 ] 1322 infoShort = info[:] 1323 1324 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1325 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1326 skippedLine = "| ... | ... | ... | ... |\n" 1327 1328 if resultsLen == 0: 1329 info.append("\nNo results\n") 1330 infoShort.append("\nNo results\n") 1331 uLogger.warning("No results. Try changing your search pattern.") 1332 1333 else: 1334 for iType in searchResults: 1335 iTypeValuesCount = len(searchResults[iType].values()) 1336 if iTypeValuesCount > 0: 1337 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1338 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 1340 for instrument in searchResults[iType].values(): 1341 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1342 instrument["type"], 1343 instrument["ticker"], 1344 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1345 instrument["figi"], 1346 )) 1347 1348 if iTypeValuesCount <= 5: 1349 infoShort.extend(info[-iTypeValuesCount:]) 1350 1351 else: 1352 infoShort.extend(info[-5:]) 1353 infoShort.append(skippedLine) 1354 1355 infoText = "".join(info) 1356 infoTextShort = "".join(infoShort) 1357 1358 if show and not onlyFiles: 1359 uLogger.info(infoTextShort) 1360 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1361 1362 if self.searchResultsFile and (show or onlyFiles): 1363 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1364 fH.write(infoText) 1365 1366 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1367 1368 if self.useHTMLReports: 1369 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1370 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1371 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1372 1373 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1374 1375 return searchResults 1376 1377 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1378 """ 1379 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1380 1381 :param instruments: list of strings with tickers or FIGIs. 1382 :return: list with unique instrument FIGIs only. 1383 """ 1384 requestedInstruments = [] 1385 for iName in instruments: 1386 if iName not in self.aliases.keys(): 1387 if iName not in requestedInstruments: 1388 requestedInstruments.append(iName) 1389 1390 else: 1391 if iName not in requestedInstruments: 1392 if self.aliases[iName] not in requestedInstruments: 1393 requestedInstruments.append(self.aliases[iName]) 1394 1395 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1396 1397 onlyUniqueFIGIs = [] 1398 for iName in requestedInstruments: 1399 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1400 continue 1401 1402 self._ticker = iName 1403 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1404 1405 if not iData: 1406 self._ticker = "" 1407 self._figi = iName 1408 1409 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1410 1411 if not iData: 1412 self._figi = "" 1413 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1414 1415 if iData and iData["figi"] not in onlyUniqueFIGIs: 1416 onlyUniqueFIGIs.append(iData["figi"]) 1417 1418 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1419 1420 return onlyUniqueFIGIs 1421 1422 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1423 """ 1424 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1425 1426 See limits: https://tinkoff.github.io/investAPI/limits/ 1427 1428 If `pricesFile` string is not empty then also save information to this file. 1429 1430 :param instruments: list of strings with tickers or FIGIs. 1431 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1432 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1433 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1434 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1435 """ 1436 if instruments is None or not instruments: 1437 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1438 raise Exception("Ticker or FIGI required") 1439 1440 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1441 1442 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1443 1444 iList = [] # trying to get info and current prices about all unique instruments: 1445 for self._figi in onlyUniqueFIGIs: 1446 iData = self.SearchByFIGI(requestPrice=True, show=False) 1447 iList.append(iData) 1448 1449 self.ShowListOfPrices(iList, show, onlyFiles) 1450 1451 return iList 1452 1453 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1454 """ 1455 Show table contains current prices of given instruments. 1456 1457 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1458 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1459 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1460 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1461 :return: multilines text in Markdown format as a table contains current prices. 1462 """ 1463 infoText = "" 1464 1465 if show or self.pricesFile or onlyFiles: 1466 info = [ 1467 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1468 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1469 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1470 ] 1471 1472 for item in iList: 1473 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1474 item["ticker"], 1475 item["figi"], 1476 item["type"], 1477 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1478 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1479 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1480 "{} / {}".format( 1481 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1482 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1483 ), 1484 "{} / {}".format( 1485 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1486 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1487 ), 1488 item["currency"], 1489 )) 1490 1491 infoText = "".join(info) 1492 1493 if show and not onlyFiles: 1494 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1495 1496 if self.pricesFile and (show or onlyFiles): 1497 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1498 fH.write(infoText) 1499 1500 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1501 1502 if self.useHTMLReports: 1503 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1504 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1505 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1506 1507 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1508 1509 return infoText 1510 1511 def RequestTradingStatus(self) -> dict: 1512 """ 1513 Requesting trading status for the instrument defined by `figi` variable. 1514 1515 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1516 1517 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1518 1519 :return: dictionary with trading status attributes. Response example: 1520 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1521 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1522 """ 1523 if self._figi is None or not self._figi: 1524 uLogger.error("Variable `figi` must be defined for using this method!") 1525 raise Exception("FIGI required") 1526 1527 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1528 1529 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1530 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1531 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1532 1533 if self.moreDebug: 1534 uLogger.debug("Records about current trading status successfully received") 1535 1536 return tradingStatus 1537 1538 def RequestPortfolio(self) -> dict: 1539 """ 1540 Requesting actual user's portfolio for current `accountId`. 1541 1542 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1543 1544 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1545 1546 :return: dictionary with user's portfolio. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1556 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1557 1558 if self.moreDebug: 1559 uLogger.debug("Records about user's portfolio successfully received") 1560 1561 return rawPortfolio 1562 1563 def RequestPositions(self) -> dict: 1564 """ 1565 Requesting open positions by currencies and instruments for current `accountId`. 1566 1567 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1568 1569 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1570 1571 :return: dictionary with open positions by instruments. 1572 """ 1573 if self.accountId is None or not self.accountId: 1574 uLogger.error("Variable `accountId` must be defined for using this method!") 1575 raise Exception("Account ID required") 1576 1577 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1578 1579 self.body = str({"accountId": self.accountId}) 1580 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1581 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1582 1583 if self.moreDebug: 1584 uLogger.debug("Records about current open positions successfully received") 1585 1586 return rawPositions 1587 1588 def RequestPendingOrders(self) -> list: 1589 """ 1590 Requesting current actual pending limit orders for current `accountId`. 1591 1592 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1593 1594 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1595 1596 :return: list of dictionaries with pending limit orders. 1597 """ 1598 if self.accountId is None or not self.accountId: 1599 uLogger.error("Variable `accountId` must be defined for using this method!") 1600 raise Exception("Account ID required") 1601 1602 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1603 1604 self.body = str({"accountId": self.accountId}) 1605 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1606 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1607 1608 if "orders" in rawResponse.keys(): 1609 rawOrders = rawResponse["orders"] 1610 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1611 1612 else: 1613 rawOrders = [] 1614 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1615 1616 return rawOrders 1617 1618 def RequestStopOrders(self) -> list: 1619 """ 1620 Requesting current actual stop orders for current `accountId`. 1621 1622 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1623 1624 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1625 1626 :return: list of dictionaries with stop orders. 1627 """ 1628 if self.accountId is None or not self.accountId: 1629 uLogger.error("Variable `accountId` must be defined for using this method!") 1630 raise Exception("Account ID required") 1631 1632 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1633 1634 self.body = str({"accountId": self.accountId}) 1635 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1636 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1637 1638 if "stopOrders" in rawResponse.keys(): 1639 rawStopOrders = rawResponse["stopOrders"] 1640 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1641 1642 else: 1643 rawStopOrders = [] 1644 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1645 1646 return rawStopOrders 1647 1648 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1649 """ 1650 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1651 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1652 and `overviewBondsCalendarFile` are defined then also save information to file. 1653 1654 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1655 many requests about the state of the portfolio, and then, based on the received data, a large number 1656 of calculation and statistics are collected. 1657 1658 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1659 :param details: how detailed should the information be? 1660 - `full` — shows full available information about portfolio status (by default), 1661 - `positions` — shows only open positions, 1662 - `orders` — shows only sections of open limits and stop orders. 1663 - `digest` — show a short digest of the portfolio status, 1664 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1665 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1666 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1667 :return: dictionary with client's raw portfolio and some statistics. 1668 """ 1669 if self.accountId is None or not self.accountId: 1670 uLogger.error("Variable `accountId` must be defined for using this method!") 1671 raise Exception("Account ID required") 1672 1673 view = { 1674 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1675 "headers": {}, # list of dictionaries, response headers without "positions" section 1676 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1677 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1678 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1679 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1680 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1681 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1682 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1683 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1684 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1685 }, 1686 "stat": { # --- some statistics calculated using "raw" sections: 1687 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1688 "availableRUB": 0., # available rubles (without other currencies) 1689 "blockedRUB": 0., # blocked sum in Russian Rouble 1690 "totalChangesRUB": 0., # changes for all open trades in RUB 1691 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1692 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1693 "sharesCostRUB": 0., # costs of all shares in RUB 1694 "bondsCostRUB": 0., # costs of all bonds in RUB 1695 "etfsCostRUB": 0., # costs of all etfs in RUB 1696 "futuresCostRUB": 0., # costs of all futures in RUB 1697 "Currencies": [], # list of dictionaries of all currencies statistics 1698 "Shares": [], # list of dictionaries of all shares statistics 1699 "Bonds": [], # list of dictionaries of all bonds statistics 1700 "Etfs": [], # list of dictionaries of all etfs statistics 1701 "Futures": [], # list of dictionaries of all futures statistics 1702 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1703 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1704 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1705 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1706 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1707 }, 1708 "analytics": { # --- some analytics of portfolio: 1709 "distrByAssets": {}, # portfolio distribution by assets 1710 "distrByCompanies": {}, # portfolio distribution by companies 1711 "distrBySectors": {}, # portfolio distribution by sectors 1712 "distrByCurrencies": {}, # portfolio distribution by currencies 1713 "distrByCountries": {}, # portfolio distribution by countries 1714 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1715 } 1716 } 1717 1718 details = details.lower() 1719 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1720 if details not in availableDetails: 1721 details = "full" 1722 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1723 1724 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1725 1726 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1727 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1728 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1729 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1730 1731 # save response headers without "positions" section: 1732 for key in portfolioResponse.keys(): 1733 if key != "positions": 1734 view["raw"]["headers"][key] = portfolioResponse[key] 1735 1736 else: 1737 continue 1738 1739 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1740 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1741 for item in portfolioResponse["positions"]: 1742 if item["instrumentType"] == "currency": 1743 self._figi = item["figi"] 1744 if not self._figi and item["ticker"]: 1745 self._ticker = item["ticker"] 1746 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1747 1748 curr = self.SearchByFIGI(requestPrice=False) 1749 1750 # current price of currency in RUB: 1751 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1752 "name": curr["name"], 1753 "currentPrice": NanoToFloat( 1754 item["currentPrice"]["units"], 1755 item["currentPrice"]["nano"] 1756 ), 1757 } 1758 1759 view["raw"]["Currencies"].append(item) 1760 1761 elif item["instrumentType"] == "share": 1762 view["raw"]["Shares"].append(item) 1763 1764 elif item["instrumentType"] == "bond": 1765 view["raw"]["Bonds"].append(item) 1766 1767 elif item["instrumentType"] == "etf": 1768 view["raw"]["Etfs"].append(item) 1769 1770 elif item["instrumentType"] == "futures": 1771 view["raw"]["Futures"].append(item) 1772 1773 else: 1774 continue 1775 1776 # how many volume of currencies (by ISO currency name) are blocked: 1777 for item in view["raw"]["positions"]["blocked"]: 1778 blocked = NanoToFloat(item["units"], item["nano"]) 1779 if blocked > 0: 1780 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1781 1782 # how many volume of instruments (by FIGI) are blocked: 1783 for item in view["raw"]["positions"]["securities"]: 1784 blocked = int(item["blocked"]) 1785 if blocked > 0: 1786 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1787 1788 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1789 1790 if "rub" in allBlocked.keys(): 1791 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1792 1793 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1794 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1795 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1796 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1797 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1798 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1799 view["stat"]["portfolioCostRUB"] = sum([ 1800 view["stat"]["allCurrenciesCostRUB"], 1801 view["stat"]["sharesCostRUB"], 1802 view["stat"]["bondsCostRUB"], 1803 view["stat"]["etfsCostRUB"], 1804 view["stat"]["futuresCostRUB"], 1805 ]) 1806 1807 # --- calculating some portfolio statistics: 1808 byComp = {} # distribution by companies 1809 bySect = {} # distribution by sectors 1810 byCurr = {} # distribution by currencies (include RUB) 1811 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1812 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1813 1814 for item in portfolioResponse["positions"]: 1815 self._figi = item["figi"] 1816 if not self._figi and item["ticker"]: 1817 self._ticker = item["ticker"] 1818 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1819 1820 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1821 1822 if instrument: 1823 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1824 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1825 1826 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1827 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1828 1829 else: 1830 blocked = 0 1831 1832 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1833 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1834 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1835 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1836 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1837 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1838 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1839 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1840 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1841 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1842 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1843 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1844 1845 statData = { 1846 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1847 "ticker": instrument["ticker"], # ticker by FIGI 1848 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1849 "volume": volume, # available volume of instrument 1850 "lots": lots, # volume in lots of instrument 1851 "direction": direction, # direction of an instrument's position: short or long 1852 "blocked": blocked, # blocked volume of currency or instrument 1853 "currentPrice": curPrice, # current instrument's price in basic asset 1854 "average": average, # current average position price 1855 "cost": cost, # current cost of all volume of instrument in basic asset 1856 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1857 "costRUB": costRUB, # cost of instrument in ruble 1858 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1859 "profit": profit, # expected profit at current moment 1860 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1861 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1862 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1863 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1864 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1865 "step": instrument["step"], # minimum price increment 1866 } 1867 1868 # adding distribution by unique countries: 1869 if statData["country"] not in byCountry.keys(): 1870 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1871 1872 else: 1873 byCountry[statData["country"]]["cost"] += costRUB 1874 byCountry[statData["country"]]["percent"] += percentCostRUB 1875 1876 if item["instrumentType"] != "currency": 1877 # adding distribution by unique companies: 1878 if statData["name"]: 1879 if statData["name"] not in byComp.keys(): 1880 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1881 1882 else: 1883 byComp[statData["name"]]["cost"] += costRUB 1884 byComp[statData["name"]]["percent"] += percentCostRUB 1885 1886 # adding distribution by unique sectors: 1887 if statData["sector"] not in bySect.keys(): 1888 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1889 1890 else: 1891 bySect[statData["sector"]]["cost"] += costRUB 1892 bySect[statData["sector"]]["percent"] += percentCostRUB 1893 1894 # adding distribution by unique currencies: 1895 if currency not in byCurr.keys(): 1896 byCurr[currency] = { 1897 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1898 "cost": costRUB, 1899 "percent": percentCostRUB 1900 } 1901 1902 else: 1903 byCurr[currency]["cost"] += costRUB 1904 byCurr[currency]["percent"] += percentCostRUB 1905 1906 # saving statistics for every instrument: 1907 if item["instrumentType"] == "currency": 1908 view["stat"]["Currencies"].append(statData) 1909 1910 # update dict with free funds for trading (total - blocked) by currencies 1911 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1912 view["stat"]["funds"][currency] = { 1913 "total": volume, 1914 "totalCostRUB": costRUB, # total volume cost in rubles 1915 "free": volume - blocked, 1916 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1917 } 1918 1919 elif item["instrumentType"] == "share": 1920 view["stat"]["Shares"].append(statData) 1921 1922 elif item["instrumentType"] == "bond": 1923 view["stat"]["Bonds"].append(statData) 1924 1925 elif item["instrumentType"] == "etf": 1926 view["stat"]["Etfs"].append(statData) 1927 1928 elif item["instrumentType"] == "Futures": 1929 view["stat"]["Futures"].append(statData) 1930 1931 else: 1932 continue 1933 1934 # total changes in Russian Ruble: 1935 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1936 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1937 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1938 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1939 view["stat"]["funds"]["rub"] = { 1940 "total": view["stat"]["availableRUB"], 1941 "totalCostRUB": view["stat"]["availableRUB"], 1942 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1943 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1944 } 1945 1946 # --- pending limit orders sector data: 1947 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1948 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1949 1950 for item in view["raw"]["orders"]: 1951 self._figi = item["figi"] 1952 1953 if item["figi"] not in uniquePendingOrdersFIGIs: 1954 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1955 1956 uniquePendingOrdersFIGIs.append(item["figi"]) 1957 uniquePendingOrders[item["figi"]] = instrument 1958 1959 else: 1960 instrument = uniquePendingOrders[item["figi"]] 1961 1962 if instrument: 1963 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1964 orderType = TKS_ORDER_TYPES[item["orderType"]] 1965 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1966 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price for order execution: 1976 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1977 1978 # necessary changes in percent to reach target from current price: 1979 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1980 1981 view["stat"]["orders"].append({ 1982 "orderID": item["orderId"], # orderId number parameter of current order 1983 "figi": item["figi"], # FIGI identification 1984 "ticker": instrument["ticker"], # ticker name by FIGI 1985 "lotsRequested": item["lotsRequested"], # requested lots value 1986 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1987 "currentPrice": lastPrice, # current instrument's price for defined action 1988 "targetPrice": target, # requested price for order execution in base currency 1989 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1990 "percentChanges": changes, # changes in percent to target from current price 1991 "currency": item["currency"], # instrument's currency name 1992 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1993 "type": orderType, # type of order from TKS_ORDER_TYPES 1994 "status": orderState, # order status from TKS_ORDER_STATES 1995 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1996 }) 1997 1998 # --- stop orders sector data: 1999 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2000 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2001 2002 for item in view["raw"]["stopOrders"]: 2003 self._figi = item["figi"] 2004 2005 if item["figi"] not in uniqueStopOrdersFIGIs: 2006 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2007 2008 uniqueStopOrdersFIGIs.append(item["figi"]) 2009 uniqueStopOrders[item["figi"]] = instrument 2010 2011 else: 2012 instrument = uniqueStopOrders[item["figi"]] 2013 2014 if instrument: 2015 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2016 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2017 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2018 2019 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2020 if "expirationTime" in item.keys(): 2021 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2022 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2023 2024 else: 2025 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2026 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2027 2028 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2029 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2030 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2031 2032 else: 2033 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2034 2035 # requested price when stop-order executed: 2036 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2037 2038 # price for limit-order, set up when stop-order executed: 2039 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2040 2041 # necessary changes in percent to reach target from current price: 2042 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2043 2044 view["stat"]["stopOrders"].append({ 2045 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2046 "figi": item["figi"], # FIGI identification 2047 "ticker": instrument["ticker"], # ticker name by FIGI 2048 "lotsRequested": item["lotsRequested"], # requested lots value 2049 "currentPrice": lastPrice, # current instrument's price for defined action 2050 "targetPrice": target, # requested price for stop-order execution in base currency 2051 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2052 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2053 "percentChanges": changes, # changes in percent to target from current price 2054 "currency": item["currency"], # instrument's currency name 2055 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2056 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2057 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2058 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2059 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2060 }) 2061 2062 # --- calculating data for analytics section: 2063 # portfolio distribution by assets: 2064 view["analytics"]["distrByAssets"] = { 2065 "Ruble": { 2066 "uniques": 1, 2067 "cost": view["stat"]["availableRUB"], 2068 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 }, 2070 "Currencies": { 2071 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2072 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2073 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2074 }, 2075 "Shares": { 2076 "uniques": len(view["stat"]["Shares"]), 2077 "cost": view["stat"]["sharesCostRUB"], 2078 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2079 }, 2080 "Bonds": { 2081 "uniques": len(view["stat"]["Bonds"]), 2082 "cost": view["stat"]["bondsCostRUB"], 2083 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2084 }, 2085 "Etfs": { 2086 "uniques": len(view["stat"]["Etfs"]), 2087 "cost": view["stat"]["etfsCostRUB"], 2088 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2089 }, 2090 "Futures": { 2091 "uniques": len(view["stat"]["Futures"]), 2092 "cost": view["stat"]["futuresCostRUB"], 2093 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2094 }, 2095 } 2096 2097 # portfolio distribution by companies: 2098 view["analytics"]["distrByCompanies"]["All money cash"] = { 2099 "ticker": "", 2100 "cost": view["stat"]["allCurrenciesCostRUB"], 2101 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2102 } 2103 view["analytics"]["distrByCompanies"].update(byComp) 2104 2105 # portfolio distribution by sectors: 2106 view["analytics"]["distrBySectors"]["All money cash"] = { 2107 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2108 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2109 } 2110 view["analytics"]["distrBySectors"].update(bySect) 2111 2112 # portfolio distribution by currencies: 2113 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2114 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2115 2116 if self.moreDebug: 2117 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2118 2119 view["analytics"]["distrByCurrencies"].update(byCurr) 2120 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2121 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2122 2123 # portfolio distribution by countries: 2124 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2125 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2126 2127 if self.moreDebug: 2128 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2129 2130 view["analytics"]["distrByCountries"].update(byCountry) 2131 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2132 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2133 2134 # --- Prepare text statistics overview in human-readable: 2135 if show or onlyFiles: 2136 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2137 2138 # Whatever the value `details`, header not changes: 2139 info = [ 2140 "# Client's portfolio\n\n", 2141 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2142 "* **Account ID:** [{}]\n".format(self.accountId), 2143 ] 2144 2145 if details in ["full", "positions", "digest"]: 2146 info.extend([ 2147 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2148 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2149 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2150 view["stat"]["totalChangesRUB"], 2151 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2152 view["stat"]["totalChangesPercentRUB"], 2153 ), 2154 ]) 2155 2156 if details in ["full", "positions"]: 2157 info.extend([ 2158 "## Open positions\n\n", 2159 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2160 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2161 "| **Ruble:** | {:>31} | | | | | |\n".format( 2162 "{:.2f} ({:.2f}) rub".format( 2163 view["stat"]["availableRUB"], 2164 view["stat"]["blockedRUB"], 2165 ) 2166 ) 2167 ]) 2168 2169 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2170 return [ 2171 "| | | | | | | |\n", 2172 "| {:<27} | | | | | {:>19} | |\n".format( 2173 noTradeStr if noTradeStr else typeStr, 2174 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2175 ), 2176 ] 2177 2178 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2179 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2180 "{} [{}]".format(data["ticker"], data["figi"]), 2181 "{:.2f} ({:.2f}) {}".format( 2182 data["volume"], 2183 data["blocked"], 2184 data["currency"], 2185 ) if isCurr else "{:.0f} ({:.0f})".format( 2186 data["volume"], 2187 data["blocked"], 2188 ), 2189 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2190 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2191 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2192 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2193 "{}{:.2f} {} ({}{:.2f}%)".format( 2194 "+" if data["profit"] > 0 else "", 2195 data["profit"], data["baseCurrencyName"], 2196 "+" if data["percentProfit"] > 0 else "", 2197 data["percentProfit"], 2198 ), 2199 ) 2200 2201 # --- Show currencies section: 2202 if view["stat"]["Currencies"]: 2203 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2204 for item in view["stat"]["Currencies"]: 2205 info.append(_InfoStr(item, isCurr=True)) 2206 2207 else: 2208 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2209 2210 # --- Show shares section: 2211 if view["stat"]["Shares"]: 2212 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2213 2214 for item in view["stat"]["Shares"]: 2215 info.append(_InfoStr(item)) 2216 2217 else: 2218 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2219 2220 # --- Show bonds section: 2221 if view["stat"]["Bonds"]: 2222 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2223 2224 for item in view["stat"]["Bonds"]: 2225 info.append(_InfoStr(item)) 2226 2227 else: 2228 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2229 2230 # --- Show etfs section: 2231 if view["stat"]["Etfs"]: 2232 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2233 2234 for item in view["stat"]["Etfs"]: 2235 info.append(_InfoStr(item)) 2236 2237 else: 2238 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2239 2240 # --- Show futures section: 2241 if view["stat"]["Futures"]: 2242 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2243 2244 for item in view["stat"]["Futures"]: 2245 info.append(_InfoStr(item)) 2246 2247 else: 2248 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2249 2250 if details in ["full", "orders"]: 2251 # --- Show pending limit orders section: 2252 if view["stat"]["orders"]: 2253 info.extend([ 2254 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2255 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2256 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2257 ]) 2258 2259 for item in view["stat"]["orders"]: 2260 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2261 "{} [{}]".format(item["ticker"], item["figi"]), 2262 item["orderID"], 2263 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2264 "{} {} ({}{:.2f}%)".format( 2265 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2266 item["baseCurrencyName"], 2267 "+" if item["percentChanges"] > 0 else "", 2268 float(item["percentChanges"]), 2269 ), 2270 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2271 item["action"], 2272 item["type"], 2273 item["date"], 2274 )) 2275 2276 else: 2277 info.append("\n## Total pending limit-orders: [0]\n") 2278 2279 # --- Show stop orders section: 2280 if view["stat"]["stopOrders"]: 2281 info.extend([ 2282 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2283 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2284 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2285 ]) 2286 2287 for item in view["stat"]["stopOrders"]: 2288 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2289 "{} [{}]".format(item["ticker"], item["figi"]), 2290 item["orderID"], 2291 item["lotsRequested"], 2292 "{} {} ({}{:.2f}%)".format( 2293 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2294 item["baseCurrencyName"], 2295 "+" if item["percentChanges"] > 0 else "", 2296 float(item["percentChanges"]), 2297 ), 2298 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2299 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2300 item["action"], 2301 item["type"], 2302 item["expType"], 2303 item["createDate"], 2304 item["expDate"], 2305 )) 2306 2307 else: 2308 info.append("\n## Total stop-orders: [0]\n") 2309 2310 if details in ["full", "analytics"]: 2311 # -- Show analytics section: 2312 if view["stat"]["portfolioCostRUB"] > 0: 2313 info.extend([ 2314 "\n# Analytics\n\n" 2315 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2316 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2317 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2318 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2319 view["stat"]["totalChangesRUB"], 2320 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2321 view["stat"]["totalChangesPercentRUB"], 2322 ), 2323 "\n## Portfolio distribution by assets\n" 2324 "\n| Type | Uniques | Percent | Current cost |\n", 2325 "|------------------------------------|---------|---------|--------------------|\n", 2326 ]) 2327 2328 for key in view["analytics"]["distrByAssets"].keys(): 2329 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2330 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2331 key, 2332 view["analytics"]["distrByAssets"][key]["uniques"], 2333 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2334 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2335 )) 2336 2337 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by companies\n" 2341 "\n| Company | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for company in view["analytics"]["distrByCompanies"].keys(): 2346 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 "{}{}".format( 2349 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2350 company, 2351 ), 2352 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2353 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2354 )) 2355 2356 info.extend([ 2357 "\n## Portfolio distribution by sectors\n" 2358 "\n| Sector | Percent | Current cost |\n", 2359 aSepLine, 2360 ]) 2361 2362 for sector in view["analytics"]["distrBySectors"].keys(): 2363 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2364 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2365 sector, 2366 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2367 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2368 )) 2369 2370 info.extend([ 2371 "\n## Portfolio distribution by currencies\n" 2372 "\n| Instruments currencies | Percent | Current cost |\n", 2373 aSepLine, 2374 ]) 2375 2376 for curr in view["analytics"]["distrByCurrencies"].keys(): 2377 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2378 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2379 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2380 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2381 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2382 )) 2383 2384 info.extend([ 2385 "\n## Portfolio distribution by countries\n" 2386 "\n| Assets by country | Percent | Current cost |\n", 2387 aSepLine, 2388 ]) 2389 2390 for country in view["analytics"]["distrByCountries"].keys(): 2391 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2392 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2393 country, 2394 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2395 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2396 )) 2397 2398 if details in ["full", "calendar"]: 2399 # -- Show bonds payment calendar section: 2400 if view["stat"]["Bonds"]: 2401 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2402 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2403 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2404 2405 else: 2406 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2407 2408 infoText = "".join(info) 2409 2410 if show and not onlyFiles: 2411 uLogger.info(infoText) 2412 2413 if details == "full" and self.overviewFile: 2414 filename = self.overviewFile 2415 2416 elif details == "digest" and self.overviewDigestFile: 2417 filename = self.overviewDigestFile 2418 2419 elif details == "positions" and self.overviewPositionsFile: 2420 filename = self.overviewPositionsFile 2421 2422 elif details == "orders" and self.overviewOrdersFile: 2423 filename = self.overviewOrdersFile 2424 2425 elif details == "analytics" and self.overviewAnalyticsFile: 2426 filename = self.overviewAnalyticsFile 2427 2428 elif details == "calendar" and self.overviewBondsCalendarFile: 2429 filename = self.overviewBondsCalendarFile 2430 2431 else: 2432 filename = "" 2433 2434 if filename and (show or onlyFiles): 2435 with open(filename, "w", encoding="UTF-8") as fH: 2436 fH.write(infoText) 2437 2438 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2439 2440 if self.useHTMLReports: 2441 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2442 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2443 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2444 2445 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2446 2447 return view 2448 2449 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2450 """ 2451 Returns history operations between two given dates for current `accountId`. 2452 If `reportFile` string is not empty then also save human-readable report. 2453 Shows some statistical data of closed positions. 2454 2455 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2456 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2457 :param show: if `True` then also prints all records to the console. 2458 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2459 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2460 :return: original list of dictionaries with history of deals records from API ("operations" key): 2461 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2462 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2463 """ 2464 if self.accountId is None or not self.accountId: 2465 uLogger.error("Variable `accountId` must be defined for using this method!") 2466 raise Exception("Account ID required") 2467 2468 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2469 2470 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2471 2472 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2473 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2474 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2475 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2476 customStat = {} # custom statistics in additional to responseJSON 2477 2478 # --- output report in human-readable format: 2479 if self.reportFile and (show or onlyFiles): 2480 splitLine1 = "| | | | | |\n" # Summary section 2481 splitLine2 = "| | | | | | | | |\n" # Operations section 2482 nextDay = "" 2483 2484 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2485 2486 if len(ops) > 0: 2487 customStat = { 2488 "opsCount": 0, # total operations count 2489 "buyCount": 0, # buy operations 2490 "sellCount": 0, # sell operations 2491 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2492 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2493 "payIn": {"rub": 0.}, # Deposit brokerage account 2494 "payOut": {"rub": 0.}, # Withdrawals 2495 "divs": {"rub": 0.}, # Dividends income 2496 "coupons": {"rub": 0.}, # Coupon's income 2497 "brokerCom": {"rub": 0.}, # Service commissions 2498 "serviceCom": {"rub": 0.}, # Service commissions 2499 "marginCom": {"rub": 0.}, # Margin commissions 2500 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2501 } 2502 2503 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2504 for item in ops: 2505 if item["state"] == "OPERATION_STATE_EXECUTED": 2506 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2507 2508 # count buy operations: 2509 if "_BUY" in item["operationType"]: 2510 customStat["buyCount"] += 1 2511 2512 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2513 customStat["buyTotal"][item["payment"]["currency"]] += payment 2514 2515 else: 2516 customStat["buyTotal"][item["payment"]["currency"]] = payment 2517 2518 # count sell operations: 2519 elif "_SELL" in item["operationType"]: 2520 customStat["sellCount"] += 1 2521 2522 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2523 customStat["sellTotal"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["sellTotal"][item["payment"]["currency"]] = payment 2527 2528 # count incoming operations: 2529 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2530 if item["payment"]["currency"] in customStat["payIn"].keys(): 2531 customStat["payIn"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["payIn"][item["payment"]["currency"]] = payment 2535 2536 # count withdrawals operations: 2537 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2538 if item["payment"]["currency"] in customStat["payOut"].keys(): 2539 customStat["payOut"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["payOut"][item["payment"]["currency"]] = payment 2543 2544 # count dividends income: 2545 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2546 if item["payment"]["currency"] in customStat["divs"].keys(): 2547 customStat["divs"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["divs"][item["payment"]["currency"]] = payment 2551 2552 # count coupon's income: 2553 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2554 if item["payment"]["currency"] in customStat["coupons"].keys(): 2555 customStat["coupons"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["coupons"][item["payment"]["currency"]] = payment 2559 2560 # count broker commissions: 2561 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2562 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2563 customStat["brokerCom"][item["payment"]["currency"]] += payment 2564 2565 else: 2566 customStat["brokerCom"][item["payment"]["currency"]] = payment 2567 2568 # count service commissions: 2569 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2570 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2571 customStat["serviceCom"][item["payment"]["currency"]] += payment 2572 2573 else: 2574 customStat["serviceCom"][item["payment"]["currency"]] = payment 2575 2576 # count margin commissions: 2577 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2578 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2579 customStat["marginCom"][item["payment"]["currency"]] += payment 2580 2581 else: 2582 customStat["marginCom"][item["payment"]["currency"]] = payment 2583 2584 # count withholding taxes: 2585 elif "_TAX" in item["operationType"]: 2586 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2587 customStat["allTaxes"][item["payment"]["currency"]] += payment 2588 2589 else: 2590 customStat["allTaxes"][item["payment"]["currency"]] = payment 2591 2592 else: 2593 continue 2594 2595 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2596 2597 # --- view "Actions" lines: 2598 info.extend([ 2599 "| Report sections | | | | |\n", 2600 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2601 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2602 "| | Buy: {:<22} | {:<28} | | |\n".format( 2603 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2604 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2605 ), 2606 "| | Sell: {:<21} | {:<28} | | |\n".format( 2607 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2608 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2609 ), 2610 ]) 2611 2612 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2613 for key in opsKeys: 2614 if key == "rub": 2615 continue 2616 2617 info.extend([ 2618 "| | | {:<28} | | |\n".format( 2619 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2620 ), 2621 "| | | {:<28} | | |\n".format( 2622 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2623 ), 2624 ]) 2625 2626 info.append(splitLine1) 2627 2628 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2629 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2630 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2631 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2632 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2633 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2634 ) 2635 2636 # --- view "Payments" lines: 2637 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2638 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2639 2640 for key in paymentsKeys: 2641 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2642 2643 info.append(splitLine1) 2644 2645 # --- view "Commissions and taxes" lines: 2646 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2647 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2648 2649 for key in comKeys: 2650 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2651 2652 info.extend([ 2653 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2654 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2655 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2656 ]) 2657 2658 else: 2659 info.append("Broker returned no operations during this period\n") 2660 2661 # --- view "Operations" section: 2662 for item in ops: 2663 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2664 continue 2665 2666 else: 2667 self._figi = item["figi"] 2668 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2669 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2670 2671 # group of deals during one day: 2672 if nextDay and item["date"].split("T")[0] != nextDay: 2673 info.append(splitLine2) 2674 nextDay = "" 2675 2676 else: 2677 nextDay = item["date"].split("T")[0] # saving current day for splitting 2678 2679 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2680 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2681 self._figi if self._figi else "—", 2682 instrument["ticker"] if instrument else "—", 2683 instrument["type"] if instrument else "—", 2684 item["quantity"] if int(item["quantity"]) > 0 else "—", 2685 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2686 TKS_OPERATION_STATES[item["state"]], 2687 TKS_OPERATION_TYPES[item["operationType"]], 2688 )) 2689 2690 infoText = "".join(info) 2691 2692 if show and not onlyFiles: 2693 if self.moreDebug: 2694 uLogger.debug("Records about history of a client's operations successfully received") 2695 2696 uLogger.info(infoText) 2697 2698 if self.reportFile and (show or onlyFiles): 2699 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2700 fH.write(infoText) 2701 2702 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2703 2704 if self.useHTMLReports: 2705 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2706 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2707 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2708 2709 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2710 2711 return ops, customStat 2712 2713 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2714 """ 2715 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2716 2717 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2718 Warning! Broker server used ISO UTC time by default. 2719 2720 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2721 Also, `historyFile` used to update history with `onlyMissing` parameter. 2722 2723 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2724 2725 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2726 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2727 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2728 `"hour"`, `"day"`. Default: `"hour"`. 2729 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2730 False by default. Warning! History appends only from last candle to current time 2731 with always update last candle! 2732 :param csvSep: separator if csv-file is used, `,` by default. 2733 :param show: if `True` then also prints Pandas DataFrame to the console. 2734 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2735 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2736 `["date", "time", "open", "high", "low", "close", "volume"]`. 2737 """ 2738 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2739 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2740 history = None # empty pandas object for history 2741 2742 if interval not in TKS_CANDLE_INTERVALS.keys(): 2743 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2744 raise Exception("Incorrect value") 2745 2746 if not (self._ticker or self._figi): 2747 uLogger.error("Ticker or FIGI must be defined!") 2748 raise Exception("Ticker or FIGI required") 2749 2750 if self._ticker and not self._figi: 2751 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2752 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2753 2754 if self._figi and not self._ticker: 2755 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2756 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2757 2758 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2759 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2760 if interval.lower() != "day": 2761 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2762 2763 delta = dtEnd - dtStart # current UTC time minus last time in file 2764 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2765 2766 # calculate history length in candles: 2767 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2768 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2769 length += 1 # to avoid fraction time 2770 2771 # calculate data blocks count: 2772 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2773 2774 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2775 if self.moreDebug: 2776 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2777 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2778 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2779 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2780 2781 tempOld = None # pandas object for old history, if --only-missing key present 2782 lastTime = None # datetime object of last old candle in file 2783 2784 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2785 if self.moreDebug: 2786 uLogger.debug("--only-missing key present, add only last missing candles...") 2787 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2788 2789 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2790 2791 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2792 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2793 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2794 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2795 2796 # get last datetime object from last string in file or minus 1 delta if file is empty: 2797 if len(tempOld) > 0: 2798 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2799 2800 else: 2801 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2802 2803 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2804 2805 responseJSONs = [] # raw history blocks of data 2806 2807 blockEnd = dtEnd 2808 for item in range(blocks): 2809 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2810 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2811 2812 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2813 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2814 )) 2815 2816 if blockStart == blockEnd: 2817 uLogger.debug("Skipped this zero-length block...") 2818 2819 else: 2820 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2821 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2822 self.body = str({ 2823 "figi": self._figi, 2824 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2825 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2826 "interval": TKS_CANDLE_INTERVALS[interval][0] 2827 }) 2828 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2829 2830 if "code" in responseJSON.keys(): 2831 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2832 2833 else: 2834 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2835 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2836 2837 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2838 2839 blockEnd = blockStart 2840 2841 printCount = len(responseJSONs) # candles to show in console 2842 if responseJSONs: 2843 tempHistory = pd.DataFrame( 2844 data={ 2845 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2846 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2847 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2848 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2849 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2850 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2851 "volume": [int(item["volume"]) for item in responseJSONs], 2852 }, 2853 index=range(len(responseJSONs)), 2854 columns=["date", "time", "open", "high", "low", "close", "volume"], 2855 ) 2856 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2857 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2858 2859 # append only newest candles to old history if --only-missing key present: 2860 if onlyMissing and tempOld is not None and lastTime is not None: 2861 index = 0 # find start index in tempHistory data: 2862 2863 for i, item in tempHistory.iterrows(): 2864 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2865 2866 if curTime == lastTime: 2867 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2868 index = i 2869 printCount = index + 1 2870 break 2871 2872 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2873 2874 else: 2875 history = tempHistory # if no `--only-missing` key then load full data from server 2876 2877 if self.moreDebug: 2878 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2879 2880 if history is not None and not history.empty: 2881 if show and not onlyFiles: 2882 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2883 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2884 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2885 )) 2886 2887 else: 2888 uLogger.warning("Received an empty candles history!") 2889 2890 if self.historyFile is not None: 2891 if history is not None and not history.empty: 2892 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2893 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2894 2895 else: 2896 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2897 2898 else: 2899 if self.moreDebug: 2900 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2901 2902 return history 2903 2904 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2905 """ 2906 Load candles history from csv-file and return Pandas DataFrame object. 2907 2908 See also: `History()` and `ShowHistoryChart()` methods. 2909 2910 :param filePath: path to csv-file to open. 2911 """ 2912 loadedHistory = None # init candles data object 2913 2914 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2915 2916 if os.path.exists(filePath): 2917 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2918 2919 tfStr = self.priceModel.FormattedDelta( 2920 self.priceModel.timeframe, 2921 "{days} days {hours}h {minutes}m {seconds}s", 2922 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2923 self.priceModel.timeframe, 2924 "{hours}h {minutes}m {seconds}s", 2925 ) 2926 2927 if loadedHistory is not None and not loadedHistory.empty: 2928 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2929 len(loadedHistory), 2930 tfStr, 2931 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2932 ) 2933 2934 else: 2935 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2936 2937 else: 2938 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2939 2940 return loadedHistory 2941 2942 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2943 """ 2944 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2945 2946 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2947 Default: `index.html` (both for interact and non-interact candlesticks chart). 2948 2949 See also: `History()` and `LoadHistory()` methods. 2950 2951 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2952 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2953 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2954 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2955 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2956 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2957 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2958 """ 2959 if isinstance(candles, str): 2960 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2961 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2962 2963 elif isinstance(candles, pd.DataFrame): 2964 self.priceModel.prices = candles # set candles chain from variable 2965 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2966 2967 if "datetime" not in candles.columns: 2968 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2969 2970 else: 2971 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2972 raise Exception("Incorrect value") 2973 2974 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2975 2976 if interact: 2977 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2978 2979 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2980 2981 else: 2982 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2983 2984 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2985 2986 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2987 2988 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2989 """ 2990 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2991 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2992 2993 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2994 2995 :param operation: string "Buy" or "Sell". 2996 :param lots: volume, integer count of lots >= 1. 2997 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2998 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2999 :param expDate: string "Undefined" by default or local date in future, 3000 it is a string with format `%Y-%m-%d %H:%M:%S`. 3001 :return: JSON with response from broker server. 3002 """ 3003 if self.accountId is None or not self.accountId: 3004 uLogger.error("Variable `accountId` must be defined for using this method!") 3005 raise Exception("Account ID required") 3006 3007 if operation is None or not operation or operation not in ("Buy", "Sell"): 3008 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3009 raise Exception("Incorrect value") 3010 3011 if lots is None or lots < 1: 3012 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3013 lots = 1 3014 3015 if tp is None or tp < 0: 3016 tp = 0 3017 3018 if sl is None or sl < 0: 3019 sl = 0 3020 3021 if expDate is None or not expDate: 3022 expDate = "Undefined" 3023 3024 if not (self._ticker or self._figi): 3025 uLogger.error("Ticker or FIGI must be defined!") 3026 raise Exception("Ticker or FIGI required") 3027 3028 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3029 self._ticker = instrument["ticker"] 3030 self._figi = instrument["figi"] 3031 3032 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3033 3034 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3035 self.body = str({ 3036 "figi": self._figi, 3037 "quantity": str(lots), 3038 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3039 "accountId": str(self.accountId), 3040 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3041 }) 3042 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3043 3044 if "orderId" in response.keys(): 3045 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3046 operation, response["orderId"], 3047 self._ticker, self._figi, lots, 3048 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3049 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3050 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3051 )) 3052 3053 if tp > 0: 3054 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3055 3056 if sl > 0: 3057 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3058 3059 else: 3060 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3061 3062 return response 3063 3064 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3065 """ 3066 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3067 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3068 3069 See also: `Order()` and `Trade()` docstrings. 3070 3071 :param lots: volume, integer count of lots >= 1. 3072 :param tp: float > 0, take profit price of stop-order. 3073 :param sl: float > 0, stop loss price of stop-order. 3074 :param expDate: it's a local date in future. 3075 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3076 :return: JSON with response from broker server. 3077 """ 3078 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3079 3080 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3081 """ 3082 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3083 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3084 3085 See also: `Order()` and `Trade()` docstrings. 3086 3087 :param lots: volume, integer count of lots >= 1. 3088 :param tp: float > 0, take profit price of stop-order. 3089 :param sl: float > 0, stop loss price of stop-order. 3090 :param expDate: it's a local date in the future. 3091 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3092 :return: JSON with response from broker server. 3093 """ 3094 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3095 3096 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3097 """ 3098 Close position of given instruments. 3099 3100 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3101 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3102 This avoids unnecessary downloading data from the server. 3103 """ 3104 if instruments is None or not instruments: 3105 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3106 raise Exception("Ticker or FIGI required") 3107 3108 if isinstance(instruments, str): 3109 instruments = [instruments] 3110 3111 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3112 if uniqueInstruments: 3113 if portfolio is None or not portfolio: 3114 portfolio = self.Overview(show=False) 3115 3116 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3117 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3118 3119 for self._figi in uniqueInstruments: 3120 if self._figi not in allOpened: 3121 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3122 continue 3123 3124 # search open trade info about instrument by ticker: 3125 instrument = {} 3126 for iType in TKS_INSTRUMENTS: 3127 if instrument: 3128 break 3129 3130 for item in portfolio["stat"][iType]: 3131 if item["figi"] == self._figi: 3132 instrument = item 3133 break 3134 3135 if instrument: 3136 self._ticker = instrument["ticker"] 3137 self._figi = instrument["figi"] 3138 3139 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3140 self._ticker, 3141 self._figi, 3142 int(instrument["volume"]), 3143 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3144 )) 3145 3146 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3147 3148 if tradeLots > 0: 3149 if instrument["blocked"] > 0: 3150 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3151 instrument["blocked"], 3152 self._ticker, 3153 tradeLots, 3154 )) 3155 3156 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3157 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3158 3159 else: 3160 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker)) 3161 3162 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3163 """ 3164 Close all positions of given instruments with defined type. 3165 3166 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3167 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3168 This avoids unnecessary downloading data from the server. 3169 """ 3170 if iType not in TKS_INSTRUMENTS: 3171 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3172 3173 else: 3174 if portfolio is None or not portfolio: 3175 portfolio = self.Overview(show=False) 3176 3177 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3178 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3179 3180 if tickers and portfolio: 3181 self.CloseTrades(tickers, portfolio) 3182 3183 else: 3184 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3185 3186 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3187 """ 3188 Universal method to create market or limit orders with all available parameters for current `accountId`. 3189 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3190 3191 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3192 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3193 3194 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3195 then broker immediately open market order as you can do simple --buy or --sell operations! 3196 3197 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3198 When current price will go up or down to target price value then broker opens a limit order. 3199 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3200 3201 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3202 3203 :param operation: string "Buy" or "Sell". 3204 :param orderType: string "Limit" or "Stop". 3205 :param lots: volume, integer count of lots >= 1. 3206 :param targetPrice: target price > 0. This is open trade price for limit order. 3207 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3208 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3209 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3210 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3211 Stop loss order always executed by market price. 3212 :param expDate: string "Undefined" by default or local date in future. 3213 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3214 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3215 A limit order has no expiration date, it lasts until the end of the trading day. 3216 :return: JSON with response from broker server. 3217 """ 3218 if self.accountId is None or not self.accountId: 3219 uLogger.error("Variable `accountId` must be defined for using this method!") 3220 raise Exception("Account ID required") 3221 3222 if operation is None or not operation or operation not in ("Buy", "Sell"): 3223 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3224 raise Exception("Incorrect value") 3225 3226 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3227 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3228 raise Exception("Incorrect value") 3229 3230 if lots is None or lots < 1: 3231 uLogger.error("You must define trade volume > 0: integer count of lots!") 3232 raise Exception("Incorrect value") 3233 3234 if targetPrice is None or targetPrice <= 0: 3235 uLogger.error("Target price for limit-order must be greater than 0!") 3236 raise Exception("Incorrect value") 3237 3238 if limitPrice is None or limitPrice <= 0: 3239 limitPrice = targetPrice 3240 3241 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3242 stopType = "Limit" 3243 3244 if expDate is None or not expDate: 3245 expDate = "Undefined" 3246 3247 if not (self._ticker or self._figi): 3248 uLogger.error("Tocker or FIGI must be defined!") 3249 raise Exception("Ticker or FIGI required") 3250 3251 response = {} 3252 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3253 self._ticker = instrument["ticker"] 3254 self._figi = instrument["figi"] 3255 3256 if orderType == "Limit": 3257 uLogger.debug( 3258 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3259 self._ticker, self._figi, 3260 operation, lots, targetPrice, instrument["currency"], 3261 )) 3262 3263 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3264 self.body = str({ 3265 "figi": self._figi, 3266 "quantity": str(lots), 3267 "price": FloatToNano(targetPrice), 3268 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3269 "accountId": str(self.accountId), 3270 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3271 }) 3272 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3273 3274 if "orderId" in response.keys(): 3275 uLogger.info( 3276 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3277 response["orderId"], self._ticker, self._figi, operation, lots, 3278 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3279 )) 3280 3281 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3282 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3283 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3284 targetPrice, instrument["currency"], 3285 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3286 )) 3287 3288 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3289 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3290 targetPrice, instrument["currency"], 3291 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3292 )) 3293 3294 else: 3295 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3296 3297 if orderType == "Stop": 3298 uLogger.debug( 3299 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3300 self._ticker, self._figi, 3301 operation, lots, 3302 targetPrice, instrument["currency"], 3303 limitPrice, instrument["currency"], 3304 stopType, expDate, 3305 )) 3306 3307 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3308 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3309 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3310 3311 body = { 3312 "figi": self._figi, 3313 "quantity": str(lots), 3314 "price": FloatToNano(limitPrice), 3315 "stopPrice": FloatToNano(targetPrice), 3316 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3317 "accountId": str(self.accountId), 3318 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3319 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3320 } 3321 3322 if expDateUTC: 3323 body["expireDate"] = expDateUTC 3324 3325 self.body = str(body) 3326 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3327 3328 if "stopOrderId" in response.keys(): 3329 uLogger.info( 3330 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3331 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3332 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3333 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3334 TKS_STOP_ORDER_TYPES[stopOrderType], 3335 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3336 )) 3337 3338 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3339 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3340 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3341 targetPrice, instrument["currency"], 3342 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3343 )) 3344 3345 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3346 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3347 targetPrice, instrument["currency"], 3348 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3349 )) 3350 3351 else: 3352 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3353 3354 return response 3355 3356 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3357 """ 3358 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3359 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3360 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3361 See also: `Order()` docstring. 3362 3363 :param lots: volume, integer count of lots >= 1. 3364 :param targetPrice: target price > 0. This is open trade price for limit order. 3365 :return: JSON with response from broker server. 3366 """ 3367 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3368 3369 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3370 """ 3371 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3372 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3373 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3374 target price value then broker opens a limit order. See also: `Order()` docstring. 3375 3376 :param lots: volume, integer count of lots >= 1. 3377 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3378 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3379 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3380 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3381 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3382 :param expDate: string "Undefined" by default or local date in future. 3383 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3384 This date is converting to UTC format for server. 3385 :return: JSON with response from broker server. 3386 """ 3387 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3388 3389 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3390 """ 3391 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3392 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3393 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3394 See also: `Order()` docstring. 3395 3396 :param lots: volume, integer count of lots >= 1. 3397 :param targetPrice: target price > 0. This is open trade price for limit order. 3398 :return: JSON with response from broker server. 3399 """ 3400 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3401 3402 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3403 """ 3404 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3405 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3406 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3407 target price value then broker opens a limit order. See also: `Order()` docstring. 3408 3409 :param lots: volume, integer count of lots >= 1. 3410 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3411 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3412 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3413 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3414 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3415 :param expDate: string "Undefined" by default or local date in future. 3416 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3417 This date is converting to UTC format for server. 3418 :return: JSON with response from broker server. 3419 """ 3420 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3421 3422 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3423 """ 3424 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3425 3426 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3427 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3428 This avoids unnecessary downloading data from the server. 3429 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3430 """ 3431 if self.accountId is None or not self.accountId: 3432 uLogger.error("Variable `accountId` must be defined for using this method!") 3433 raise Exception("Account ID required") 3434 3435 if orderIDs: 3436 if allOrdersIDs is None: 3437 rawOrders = self.RequestPendingOrders() 3438 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3439 3440 if allStopOrdersIDs is None: 3441 rawStopOrders = self.RequestStopOrders() 3442 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3443 3444 for orderID in orderIDs: 3445 idInPendingOrders = orderID in allOrdersIDs 3446 idInStopOrders = orderID in allStopOrdersIDs 3447 3448 if not (idInPendingOrders or idInStopOrders): 3449 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3450 continue 3451 3452 else: 3453 if idInPendingOrders: 3454 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3455 3456 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3457 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3458 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3459 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3460 3461 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3462 if self.moreDebug: 3463 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3464 3465 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3466 3467 else: 3468 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3469 3470 elif idInStopOrders: 3471 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3472 3473 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3474 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3475 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3476 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3477 3478 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3479 if self.moreDebug: 3480 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3481 3482 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3483 3484 else: 3485 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3486 3487 else: 3488 continue 3489 3490 def CloseAllOrders(self) -> None: 3491 """ 3492 Gets a list of open pending and stop orders and cancel it all. 3493 """ 3494 rawOrders = self.RequestPendingOrders() 3495 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3496 lenOrders = len(allOrdersIDs) 3497 3498 rawStopOrders = self.RequestStopOrders() 3499 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3500 lenSOrders = len(allStopOrdersIDs) 3501 3502 if lenOrders > 0 or lenSOrders > 0: 3503 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3504 3505 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3506 3507 else: 3508 uLogger.info("Orders not found, nothing to cancel.") 3509 3510 def CloseAll(self, *args) -> None: 3511 """ 3512 Close all available (not blocked) opened trades and orders. 3513 3514 Also, you can select one or more keywords case-insensitive: 3515 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3516 3517 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3518 """ 3519 overview = self.Overview(show=False) # get all open trades info 3520 3521 if len(args) == 0: 3522 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3523 self.CloseAllOrders() # close all pending and stop orders 3524 3525 for iType in TKS_INSTRUMENTS: 3526 if iType != "Currencies": 3527 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3528 3529 else: 3530 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3531 lowerArgs = [x.lower() for x in args] 3532 3533 if "orders" in lowerArgs: 3534 self.CloseAllOrders() # close all pending and stop orders 3535 3536 for iType in TKS_INSTRUMENTS: 3537 if iType.lower() in lowerArgs and iType != "Currencies": 3538 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3539 3540 def CloseAllByTicker(self, instrument: str) -> None: 3541 """ 3542 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3543 3544 This method searches opened trade and orders of instrument throw all portfolio and then use 3545 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3546 3547 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3548 3549 :param instrument: string with ticker. 3550 """ 3551 if instrument is None or not instrument: 3552 uLogger.error("Ticker name must be defined for using this method!") 3553 raise Exception("Ticker required") 3554 3555 overview = self.Overview(show=False) # get user portfolio with all open trades info 3556 3557 self._ticker = instrument # try to set instrument as ticker 3558 self._figi = "" 3559 3560 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3561 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3562 3563 if limitAll and self.IsInLimitOrders(portfolio=overview): 3564 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3565 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3566 3567 if stopAll and self.IsInStopOrders(portfolio=overview): 3568 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3569 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3570 3571 if self.IsInPortfolio(portfolio=overview): 3572 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3573 self.CloseTrades(instruments=[instrument], portfolio=overview) 3574 3575 def CloseAllByFIGI(self, instrument: str) -> None: 3576 """ 3577 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3578 3579 This method searches opened trade and orders of instrument throw all portfolio and then use 3580 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3581 3582 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3583 3584 :param instrument: string with FIGI id. 3585 """ 3586 if instrument is None or not instrument: 3587 uLogger.error("FIGI id must be defined for using this method!") 3588 raise Exception("FIGI required") 3589 3590 overview = self.Overview(show=False) # get user portfolio with all open trades info 3591 3592 self._ticker = "" 3593 self._figi = instrument # try to set instrument as FIGI id 3594 3595 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3596 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3597 3598 if limitAll and self.IsInLimitOrders(portfolio=overview): 3599 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3600 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3601 3602 if stopAll and self.IsInStopOrders(portfolio=overview): 3603 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3604 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3605 3606 if self.IsInPortfolio(portfolio=overview): 3607 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3608 self.CloseTrades(instruments=[instrument], portfolio=overview) 3609 3610 @staticmethod 3611 def ParseOrderParameters(operation, **inputParameters): 3612 """ 3613 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3614 3615 :param operation: string "Buy" or "Sell". 3616 :param inputParameters: this is dict of strings that looks like this 3617 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3618 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3619 "prices" key: one or more prices to open limit-orders 3620 Counts of values in lots and prices lists must be equals! 3621 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3622 """ 3623 # TODO: update order grid work with api v2 3624 pass 3625 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3626 # 3627 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3628 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3629 # raise Exception("Incorrect value") 3630 # 3631 # if "l" in inputParameters.keys(): 3632 # inputParameters["lots"] = inputParameters.pop("l") 3633 # 3634 # if "p" in inputParameters.keys(): 3635 # inputParameters["prices"] = inputParameters.pop("p") 3636 # 3637 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3638 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3639 # raise Exception("Incorrect value") 3640 # 3641 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3642 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3643 # 3644 # if len(lots) != len(prices): 3645 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3646 # raise Exception("Incorrect value") 3647 # 3648 # uLogger.debug("Extracted parameters for orders:") 3649 # uLogger.debug("lots = {}".format(lots)) 3650 # uLogger.debug("prices = {}".format(prices)) 3651 # 3652 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3653 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3654 # uLogger.debug("Order parameters: {}".format(result)) 3655 # 3656 # return result 3657 3658 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3659 """ 3660 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3661 3662 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3663 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3664 """ 3665 result = False 3666 msg = "Instrument not defined!" 3667 3668 if portfolio is None or not portfolio: 3669 portfolio = self.Overview(show=False) 3670 3671 if self._ticker: 3672 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3673 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3674 3675 for iType in TKS_INSTRUMENTS: 3676 for instrument in portfolio["stat"][iType]: 3677 if instrument["ticker"] == self._ticker: 3678 result = True 3679 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3680 break 3681 3682 elif self._figi: 3683 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3684 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3685 3686 for iType in TKS_INSTRUMENTS: 3687 for instrument in portfolio["stat"][iType]: 3688 if instrument["figi"] == self._figi: 3689 result = True 3690 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3691 break 3692 3693 else: 3694 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3695 3696 uLogger.debug(msg) 3697 3698 return result 3699 3700 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3701 """ 3702 Returns instrument from the user's portfolio if it presents there. 3703 Instrument must be defined by `ticker` (highly priority) or `figi`. 3704 3705 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3706 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3707 """ 3708 result = None 3709 msg = "Instrument not defined!" 3710 3711 if portfolio is None or not portfolio: 3712 portfolio = self.Overview(show=False) 3713 3714 if self._ticker: 3715 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3716 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3717 3718 for iType in TKS_INSTRUMENTS: 3719 for instrument in portfolio["stat"][iType]: 3720 if instrument["ticker"] == self._ticker: 3721 result = instrument 3722 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3723 break 3724 3725 elif self._figi: 3726 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3727 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3728 3729 for iType in TKS_INSTRUMENTS: 3730 for instrument in portfolio["stat"][iType]: 3731 if instrument["figi"] == self._figi: 3732 result = instrument 3733 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3734 break 3735 3736 else: 3737 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3738 3739 uLogger.debug(msg) 3740 3741 return result 3742 3743 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3744 """ 3745 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3746 3747 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3748 3749 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3750 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3751 """ 3752 result = False 3753 msg = "Instrument not defined!" 3754 3755 if portfolio is None or not portfolio: 3756 portfolio = self.Overview(show=False) 3757 3758 if self._ticker: 3759 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3760 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3761 3762 for instrument in portfolio["stat"]["orders"]: 3763 if instrument["ticker"] == self._ticker: 3764 result = True 3765 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3766 break 3767 3768 elif self._figi: 3769 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3770 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3771 3772 for instrument in portfolio["stat"]["orders"]: 3773 if instrument["figi"] == self._figi: 3774 result = True 3775 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3776 break 3777 3778 else: 3779 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3780 3781 uLogger.debug(msg) 3782 3783 return result 3784 3785 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3786 """ 3787 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3788 Instrument must be defined by `ticker` (highly priority) or `figi`. 3789 3790 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3791 3792 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3793 :return: list with `orderID`s of limit orders. 3794 """ 3795 result = [] 3796 msg = "Instrument not defined!" 3797 3798 if portfolio is None or not portfolio: 3799 portfolio = self.Overview(show=False) 3800 3801 if self._ticker: 3802 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3803 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3804 3805 for instrument in portfolio["stat"]["orders"]: 3806 if instrument["ticker"] == self._ticker: 3807 result.append(instrument["orderID"]) 3808 3809 if result: 3810 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3811 3812 elif self._figi: 3813 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3814 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3815 3816 for instrument in portfolio["stat"]["orders"]: 3817 if instrument["figi"] == self._figi: 3818 result.append(instrument["orderID"]) 3819 3820 if result: 3821 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3822 3823 else: 3824 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3825 3826 uLogger.debug(msg) 3827 3828 return result 3829 3830 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3831 """ 3832 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3833 3834 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3835 3836 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3837 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3838 """ 3839 result = False 3840 msg = "Instrument not defined!" 3841 3842 if portfolio is None or not portfolio: 3843 portfolio = self.Overview(show=False) 3844 3845 if self._ticker: 3846 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3847 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3848 3849 for instrument in portfolio["stat"]["stopOrders"]: 3850 if instrument["ticker"] == self._ticker: 3851 result = True 3852 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3853 break 3854 3855 elif self._figi: 3856 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3857 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3858 3859 for instrument in portfolio["stat"]["stopOrders"]: 3860 if instrument["figi"] == self._figi: 3861 result = True 3862 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3863 break 3864 3865 else: 3866 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3867 3868 uLogger.debug(msg) 3869 3870 return result 3871 3872 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3873 """ 3874 Returns list with all `orderID`s of opened stop orders for the instrument. 3875 Instrument must be defined by `ticker` (highly priority) or `figi`. 3876 3877 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3878 3879 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3880 :return: list with `orderID`s of stop orders. 3881 """ 3882 result = [] 3883 msg = "Instrument not defined!" 3884 3885 if portfolio is None or not portfolio: 3886 portfolio = self.Overview(show=False) 3887 3888 if self._ticker: 3889 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3890 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3891 3892 for instrument in portfolio["stat"]["stopOrders"]: 3893 if instrument["ticker"] == self._ticker: 3894 result.append(instrument["orderID"]) 3895 3896 if result: 3897 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3898 3899 elif self._figi: 3900 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3901 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3902 3903 for instrument in portfolio["stat"]["stopOrders"]: 3904 if instrument["figi"] == self._figi: 3905 result.append(instrument["orderID"]) 3906 3907 if result: 3908 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3909 3910 else: 3911 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3912 3913 uLogger.debug(msg) 3914 3915 return result 3916 3917 def RequestLimits(self) -> dict: 3918 """ 3919 Method for obtaining the available funds for withdrawal for current `accountId`. 3920 3921 See also: 3922 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3923 - `OverviewLimits()` method 3924 3925 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3926 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3927 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3928 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3929 """ 3930 if self.accountId is None or not self.accountId: 3931 uLogger.error("Variable `accountId` must be defined for using this method!") 3932 raise Exception("Account ID required") 3933 3934 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3935 3936 self.body = str({"accountId": self.accountId}) 3937 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3938 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3939 3940 if self.moreDebug: 3941 uLogger.debug("Records about available funds for withdrawal successfully received") 3942 3943 return rawLimits 3944 3945 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3946 """ 3947 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3948 3949 See also: `RequestLimits()`. 3950 3951 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3952 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3953 :return: dict with raw parsed data from server and some calculated statistics about it. 3954 """ 3955 if self.accountId is None or not self.accountId: 3956 uLogger.error("Variable `accountId` must be defined for using this method!") 3957 raise Exception("Account ID required") 3958 3959 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3960 3961 view = { 3962 "rawLimits": rawLimits, 3963 "limits": { # parsed data for every currency: 3964 "money": { # this is an array of portfolio currency positions 3965 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3966 }, 3967 "blocked": { # this is an array of blocked currency 3968 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3969 }, 3970 "blockedGuarantee": { # this is locked money under collateral for futures 3971 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3972 }, 3973 }, 3974 } 3975 3976 # --- Prepare text table with limits in human-readable format: 3977 if show or onlyFiles: 3978 info = [ 3979 "# Withdrawal limits\n\n", 3980 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3981 "* **Account ID:** [{}]\n".format(self.accountId), 3982 ] 3983 3984 if view["limits"]["money"]: 3985 info.extend([ 3986 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3987 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3988 ]) 3989 3990 else: 3991 info.append("\nNo withdrawal limits\n") 3992 3993 for curr in view["limits"]["money"].keys(): 3994 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3995 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3996 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3997 3998 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3999 "[{}]".format(curr), 4000 "{:.2f}".format(view["limits"]["money"][curr]), 4001 "{:.2f}".format(availableMoney), 4002 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4003 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4004 ) 4005 4006 if curr == "rub": 4007 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4008 4009 else: 4010 info.append(infoStr) 4011 4012 infoText = "".join(info) 4013 4014 if show and not onlyFiles: 4015 uLogger.info(infoText) 4016 4017 if self.withdrawalLimitsFile and (show or onlyFiles): 4018 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4019 fH.write(infoText) 4020 4021 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4022 4023 if self.useHTMLReports: 4024 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4025 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4026 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4027 4028 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4029 4030 return view 4031 4032 def RequestAccounts(self) -> dict: 4033 """ 4034 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4035 4036 See also: 4037 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4038 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4039 - `OverviewUserInfo()` method 4040 4041 :return: dict with raw data from server that contains accounts info. Example of dict: 4042 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4043 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4044 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4045 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4046 """ 4047 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4048 4049 self.body = str({}) 4050 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4051 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4052 4053 if self.moreDebug: 4054 uLogger.debug("Records about available accounts successfully received") 4055 4056 return rawAccounts 4057 4058 def RequestUserInfo(self) -> dict: 4059 """ 4060 Method for requesting common user's information. 4061 4062 See also: 4063 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4064 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4065 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4066 - `OverviewUserInfo()` method 4067 4068 :return: dict with raw data from server that contains user's information. Example of dict: 4069 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4070 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4071 """ 4072 uLogger.debug("Requesting common user's information. Wait, please...") 4073 4074 self.body = str({}) 4075 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4076 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4077 4078 if self.moreDebug: 4079 uLogger.debug("Records about current user successfully received") 4080 4081 return rawUserInfo 4082 4083 def RequestMarginStatus(self, accountId: str = None) -> dict: 4084 """ 4085 Method for requesting margin calculation for defined account ID. 4086 4087 See also: 4088 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4089 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4090 - `OverviewUserInfo()` method 4091 4092 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4093 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4094 Example of responses: 4095 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4096 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4097 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4098 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4099 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4100 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4101 """ 4102 if accountId is None or not accountId: 4103 if self.accountId is None or not self.accountId: 4104 uLogger.error("Variable `accountId` must be defined for using this method!") 4105 raise Exception("Account ID required") 4106 4107 else: 4108 accountId = self.accountId # use `self.accountId` (main ID) by default 4109 4110 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4111 4112 self.body = str({"accountId": accountId}) 4113 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4114 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4115 4116 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4117 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4118 rawMargin = {} 4119 4120 else: 4121 if self.moreDebug: 4122 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4123 4124 return rawMargin 4125 4126 def RequestTariffLimits(self) -> dict: 4127 """ 4128 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4129 4130 See also: 4131 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4132 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4133 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4134 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4135 - `OverviewUserInfo()` method 4136 4137 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4138 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4139 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4140 """ 4141 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4142 4143 self.body = str({}) 4144 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4145 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4146 4147 if self.moreDebug: 4148 uLogger.debug("Records with limits of current tariff successfully received") 4149 4150 return rawTariffLimits 4151 4152 def RequestBondCoupons(self, iJSON: dict) -> dict: 4153 """ 4154 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4155 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4156 All dates are in UTC timezone. 4157 4158 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4159 Documentation: 4160 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4161 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4162 4163 See also: `ExtendBondsData()`. 4164 4165 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4166 If raw iJSON is not data of bond then server returns an error [400] with message: 4167 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4168 :return: dictionary with bond payment calendar. Response example 4169 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4170 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4171 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4172 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4173 """ 4174 if iJSON["figi"] is None or not iJSON["figi"]: 4175 uLogger.error("FIGI must be defined for using this method!") 4176 raise Exception("FIGI required") 4177 4178 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4179 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4180 4181 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4182 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4183 self._figi, 4184 startDate, 4185 endDate, 4186 )) 4187 4188 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4189 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4190 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4191 4192 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4193 uLogger.warning("Instrument type is not bond!") 4194 4195 else: 4196 if self.moreDebug: 4197 uLogger.debug("Records about bond payment calendar successfully received") 4198 4199 return calendar 4200 4201 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4202 """ 4203 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4204 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4205 coupon yields, current yields and some statistics etc. 4206 4207 WARNING! This is too long operation if a lot of bonds requested from broker server. 4208 4209 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4210 4211 :param instruments: list of strings with tickers or FIGIs. 4212 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4213 for further used by data scientists or stock analytics. 4214 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4215 In XLSX-file and Pandas DataFrame fields mean: 4216 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4217 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4218 """ 4219 if instruments is None or not instruments: 4220 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4221 raise Exception("Ticker or FIGI required") 4222 4223 if isinstance(instruments, str): 4224 instruments = [instruments] 4225 4226 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4227 4228 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4229 4230 iCount = len(uniqueInstruments) 4231 tooLong = iCount >= 20 4232 if tooLong: 4233 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4234 4235 bonds = None 4236 for i, self._figi in enumerate(uniqueInstruments): 4237 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4238 4239 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4240 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4241 rawBond = self.SearchByFIGI(requestPrice=True) 4242 4243 # Widen raw data with UTC current time (iData["actualDateTime"]): 4244 actualDate = datetime.now(tzutc()) 4245 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4246 4247 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4248 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4249 4250 # Replace some values with human-readable: 4251 iData["nominalCurrency"] = iData["nominal"]["currency"] 4252 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4253 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4254 iData["aciCurrency"] = iData["aciValue"]["currency"] 4255 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4256 iData["issueSize"] = int(iData["issueSize"]) 4257 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4258 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4259 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4260 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4261 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4262 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4263 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4264 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4265 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4266 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4267 4268 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4269 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4270 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4271 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4272 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4273 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4274 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4275 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4276 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4277 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4278 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4279 4280 # Widen raw data with calendar data from `rawCalendar` values: 4281 calendarData = [] 4282 if "events" in iData["rawCalendar"].keys(): 4283 for item in iData["rawCalendar"]["events"]: 4284 calendarData.append({ 4285 "couponDate": item["couponDate"], 4286 "couponNumber": int(item["couponNumber"]), 4287 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4288 "payCurrency": item["payOneBond"]["currency"], 4289 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4290 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4291 "couponStartDate": item["couponStartDate"], 4292 "couponEndDate": item["couponEndDate"], 4293 "couponPeriod": item["couponPeriod"], 4294 }) 4295 4296 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4297 if "maturityDate" not in iData.keys(): 4298 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4299 4300 # Widen raw data with Coupon Rate. 4301 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4302 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4303 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4304 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4305 4306 # Widen raw data with Yield to Maturity (YTM) on current date. 4307 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4308 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4309 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4310 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4311 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4312 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4313 4314 iData["calendar"] = calendarData # adds calendar at the end 4315 4316 # Remove not used data: 4317 iData.pop("uid") 4318 iData.pop("positionUid") 4319 iData.pop("currentPrice") 4320 iData.pop("rawCalendar") 4321 4322 colNames = list(iData.keys()) 4323 if bonds is None: 4324 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4325 4326 else: 4327 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4328 4329 else: 4330 uLogger.warning("Instrument is not a bond!") 4331 4332 processed = round(100 * (i + 1) / iCount, 1) 4333 if tooLong and processed % 5 == 0: 4334 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4335 4336 else: 4337 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4338 4339 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4340 4341 # Saving bonds from Pandas DataFrame to XLSX sheet: 4342 if xlsx and self.bondsXLSXFile: 4343 with pd.ExcelWriter( 4344 path=self.bondsXLSXFile, 4345 date_format=TKS_DATE_FORMAT, 4346 datetime_format=TKS_DATE_TIME_FORMAT, 4347 mode="w", 4348 ) as writer: 4349 bonds.to_excel( 4350 writer, 4351 sheet_name="Extended bonds data", 4352 index=True, 4353 encoding="UTF-8", 4354 freeze_panes=(1, 1), 4355 ) # saving as XLSX-file with freeze first row and column as headers 4356 4357 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4358 4359 return bonds 4360 4361 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4362 """ 4363 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4364 4365 WARNING! This is too long operation if a lot of bonds requested from broker server. 4366 4367 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4368 4369 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4370 extended information about bonds: main info, current prices, bond payment calendar, 4371 coupon yields, current yields and some statistics etc. 4372 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4373 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4374 for further used by data scientists or stock analytics. 4375 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4376 """ 4377 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4378 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4379 4380 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4381 4382 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4383 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4384 calendar = None 4385 for bond in extBonds.iterrows(): 4386 for item in bond[1]["calendar"]: 4387 cData = { 4388 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4389 "couponDate": item["couponDate"], 4390 "figi": bond[1]["figi"], 4391 "ticker": bond[1]["ticker"], 4392 "name": bond[1]["name"], 4393 "couponNumber": item["couponNumber"], 4394 "payOneBond": item["payOneBond"], 4395 "payCurrency": item["payCurrency"], 4396 "couponType": item["couponType"], 4397 "couponPeriod": item["couponPeriod"], 4398 "fixDate": item["fixDate"], 4399 "couponStartDate": item["couponStartDate"], 4400 "couponEndDate": item["couponEndDate"], 4401 } 4402 4403 if calendar is None: 4404 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4405 4406 else: 4407 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4408 4409 if calendar is not None: 4410 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4411 4412 # Saving calendar from Pandas DataFrame to XLSX sheet: 4413 if xlsx: 4414 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4415 4416 with pd.ExcelWriter( 4417 path=xlsxCalendarFile, 4418 date_format=TKS_DATE_FORMAT, 4419 datetime_format=TKS_DATE_TIME_FORMAT, 4420 mode="w", 4421 ) as writer: 4422 humanReadable = calendar.copy(deep=True) 4423 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4424 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4425 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable.columns = colNames # human-readable column names 4428 4429 humanReadable.to_excel( 4430 writer, 4431 sheet_name="Bond payments calendar", 4432 index=False, 4433 encoding="UTF-8", 4434 freeze_panes=(1, 2), 4435 ) # saving as XLSX-file with freeze first row and column as headers 4436 4437 del humanReadable # release df in memory 4438 4439 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4440 4441 return calendar 4442 4443 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4444 """ 4445 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4446 Also, creates Markdown file with calendar data, `calendar.md` by default. 4447 4448 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4449 4450 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4451 extended information about bonds: main info, current prices, bond payment calendar, 4452 coupon yields, current yields and some statistics etc. 4453 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4454 :param show: if `True` then also printing bonds payment calendar to the console, 4455 otherwise save to file `calendarFile` only. `False` by default. 4456 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4457 :return: multilines text in Markdown format with bonds payment calendar as a table. 4458 """ 4459 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4460 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4461 4462 infoText = "# Bond payments calendar\n\n" 4463 4464 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4465 4466 if not (calendar is None or calendar.empty): 4467 splitLine = "| | | | | | | | | |\n" 4468 4469 info = [ 4470 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4471 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4472 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4473 ] 4474 4475 newMonth = False 4476 notOneBond = calendar["figi"].nunique() > 1 4477 for i, bond in enumerate(calendar.iterrows()): 4478 if newMonth and notOneBond: 4479 info.append(splitLine) 4480 4481 info.append( 4482 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4483 " √" if bond[1]["paid"] else " —", 4484 bond[1]["couponDate"].split("T")[0], 4485 bond[1]["figi"], 4486 bond[1]["ticker"], 4487 bond[1]["couponNumber"], 4488 "{} {}".format( 4489 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4490 bond[1]["payCurrency"], 4491 ), 4492 bond[1]["couponType"], 4493 bond[1]["couponPeriod"], 4494 bond[1]["fixDate"].split("T")[0], 4495 ) 4496 ) 4497 4498 if i < len(calendar.values) - 1: 4499 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4500 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4501 newMonth = False if curDate.month == nextDate.month else True 4502 4503 else: 4504 newMonth = False 4505 4506 infoText += "".join(info) 4507 4508 if show and not onlyFiles: 4509 uLogger.info("{}".format(infoText)) 4510 4511 if self.calendarFile is not None and (show or onlyFiles): 4512 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4513 fH.write(infoText) 4514 4515 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4516 4517 if self.useHTMLReports: 4518 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4519 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4520 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4521 4522 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4523 4524 else: 4525 infoText += "No data\n" 4526 4527 return infoText 4528 4529 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4530 """ 4531 Method for parsing and show simple table with all available user accounts. 4532 4533 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4534 4535 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4536 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4537 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4538 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4539 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4540 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4541 "closed": "—", "access": "Full access" }, ...}}` 4542 """ 4543 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4544 4545 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4546 accounts = { 4547 item["id"]: { 4548 "type": TKS_ACCOUNT_TYPES[item["type"]], 4549 "name": item["name"], 4550 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4551 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4552 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4553 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4554 } for item in rawAccounts["accounts"] 4555 } 4556 4557 # Raw and parsed data with some fields replaced in "stat" section: 4558 view = { 4559 "rawAccounts": rawAccounts, 4560 "stat": accounts, 4561 } 4562 4563 # --- Prepare simple text table with only accounts data in human-readable format: 4564 if show or onlyFiles: 4565 info = [ 4566 "# User accounts\n\n", 4567 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4568 "| Account ID | Type | Status | Name |\n", 4569 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4570 ] 4571 4572 for account in view["stat"].keys(): 4573 info.extend([ 4574 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4575 account, 4576 view["stat"][account]["type"], 4577 view["stat"][account]["status"], 4578 view["stat"][account]["name"], 4579 ) 4580 ]) 4581 4582 infoText = "".join(info) 4583 4584 if show and not onlyFiles: 4585 uLogger.info(infoText) 4586 4587 if self.userAccountsFile and (show or onlyFiles): 4588 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4589 fH.write(infoText) 4590 4591 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4592 4593 if self.useHTMLReports: 4594 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4595 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4596 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4597 4598 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4599 4600 return view 4601 4602 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4603 """ 4604 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4605 4606 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4607 4608 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4609 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4610 :return: dict with raw parsed data from server and some calculated statistics about it. 4611 """ 4612 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4613 tmpTicker = self._ticker 4614 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4615 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4616 self._ticker = tmpTicker 4617 4618 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4619 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4620 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4621 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4622 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4623 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4624 4625 # This is dict with parsed common user data: 4626 userInfo = { 4627 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4628 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4629 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4630 "tariff": rawUserInfo["tariff"], 4631 } 4632 4633 # This is an array of dict with parsed margin statuses for every account IDs: 4634 margins = {} 4635 for accountId in accounts.keys(): 4636 if rawMargins[accountId]: 4637 margins[accountId] = { 4638 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4639 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4640 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4641 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4642 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4643 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4644 "missing": missing["volume"], 4645 } 4646 4647 else: 4648 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4649 4650 unary = {} # unary-connection limits 4651 for item in rawTariffLimits["unaryLimits"]: 4652 if item["limitPerMinute"] in unary.keys(): 4653 unary[item["limitPerMinute"]].extend(item["methods"]) 4654 4655 else: 4656 unary[item["limitPerMinute"]] = item["methods"] 4657 4658 stream = {} # stream-connection limits 4659 for item in rawTariffLimits["streamLimits"]: 4660 if item["limit"] in stream.keys(): 4661 stream[item["limit"]].extend(item["streams"]) 4662 4663 else: 4664 stream[item["limit"]] = item["streams"] 4665 4666 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4667 limits = { 4668 "unary": unary, 4669 "stream": stream, 4670 } 4671 4672 # Raw and parsed data as an output result: 4673 view = { 4674 "rawUserInfo": rawUserInfo, 4675 "rawAccounts": rawAccounts, 4676 "rawMargins": rawMargins, 4677 "rawTariffLimits": rawTariffLimits, 4678 "stat": { 4679 "overview": overview, 4680 "userInfo": userInfo, 4681 "accounts": accounts, 4682 "margins": margins, 4683 "limits": limits, 4684 }, 4685 } 4686 4687 # --- Prepare text table with user information in human-readable format: 4688 if show or onlyFiles: 4689 info = [ 4690 "# Full user information\n\n", 4691 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4692 "## Common information\n\n", 4693 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4694 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4695 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4696 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4697 "\n## User accounts\n\n", 4698 ] 4699 4700 for account in view["stat"]["accounts"].keys(): 4701 info.extend([ 4702 "### ID: [{}]\n\n".format(account), 4703 "| Parameters | Values |\n", 4704 "|----------------------|--------------------------------------------------------------|\n", 4705 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4706 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4707 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4708 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4709 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4710 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4711 ]) 4712 4713 if margins[account]: 4714 info.extend([ 4715 "| Margin status: | Enabled |\n", 4716 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4717 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4718 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4719 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4720 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4721 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4722 ]) 4723 4724 else: 4725 info.append("| Margin status: | Disabled |\n\n") 4726 4727 info.extend([ 4728 "\n## Current user tariff limits\n", 4729 "\n### See also\n", 4730 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4731 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4732 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4733 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4734 "\n### Unary limits\n", 4735 ]) 4736 4737 if unary: 4738 for key, values in sorted(unary.items()): 4739 info.append("\n* Max requests per minute: {}\n".format(key)) 4740 4741 for value in values: 4742 info.append(" - {}\n".format(value)) 4743 4744 else: 4745 info.append("\nNot available\n") 4746 4747 info.append("\n### Stream limits\n") 4748 4749 if stream: 4750 for key, values in sorted(stream.items()): 4751 info.append("\n* Max stream connections: {}\n".format(key)) 4752 4753 for value in values: 4754 info.append(" - {}\n".format(value)) 4755 4756 else: 4757 info.append("\nNot available\n") 4758 4759 infoText = "".join(info) 4760 4761 if show and not onlyFiles: 4762 uLogger.info(infoText) 4763 4764 if self.userInfoFile and (show or onlyFiles): 4765 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4766 fH.write(infoText) 4767 4768 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4769 4770 if self.useHTMLReports: 4771 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4772 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4773 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4774 4775 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4776 4777 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
86 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 87 """ 88 Main class init. 89 90 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 91 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 92 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 93 :param useCache: use default cache file with raw data to use instead of `iList`. 94 True by default. Cache is auto-update if new day has come. 95 If you don't want to use cache and always updates raw data then set `useCache=False`. 96 :param defaultCache: path to default cache file. `dump.json` by default. 97 """ 98 if token is None or not token: 99 try: 100 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 101 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 102 103 except KeyError: 104 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 105 raise Exception("Token required") 106 107 else: 108 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 109 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 110 111 if accountId is None or not accountId: 112 try: 113 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 114 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 115 116 except KeyError: 117 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 118 119 else: 120 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 121 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 122 123 self.version = __version__ # duplicate here used TKSBrokerAPI main version 124 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 125 126 Latest version: https://pypi.org/project/tksbrokerapi/ 127 """ 128 129 self._tag = "" 130 """Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).""" 131 132 self.__lock = Lock() # initialize multiprocessing mutex lock 133 134 self.aliases = TKS_TICKER_ALIASES 135 """Some aliases instead official tickers. 136 137 See also: `TKSEnums.TKS_TICKER_ALIASES` 138 """ 139 140 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 141 142 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 143 144 self._ticker = "" 145 """String with ticker, e.g. `GOOGL`. Tickers may be upper case only. 146 147 Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. 148 More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 149 150 See also: `SearchByTicker()`, `SearchInstruments()`. 151 """ 152 153 self._figi = "" 154 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only. 155 156 See also: `SearchByFIGI()`, `SearchInstruments()`. 157 """ 158 159 self.depth = 1 160 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 161 162 See also: `GetCurrentPrices()`. 163 """ 164 165 self.server = r"https://invest-public-api.tinkoff.ru/rest" 166 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 167 168 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 169 """ 170 171 uLogger.debug("Broker API server: {}".format(self.server)) 172 173 self.timeout = 15 174 """Server operations timeout in seconds. Default: `15`. 175 176 See also: `SendAPIRequest()`. 177 """ 178 179 self.headers = { 180 "Content-Type": "application/json", 181 "accept": "application/json", 182 "Authorization": "Bearer {}".format(self.token), 183 "x-app-name": "Tim55667757.TKSBrokerAPI", 184 } 185 """ 186 Headers which send in every request to broker server. Please, do not change it! 187 Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}`. 188 189 See also: `SendAPIRequest()`. 190 """ 191 192 self.body = None 193 """Request body which send to broker server. Default: `None`. 194 195 See also: `SendAPIRequest()`. 196 """ 197 198 self.moreDebug = False 199 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 200 201 self.useHTMLReports = False 202 """ 203 If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default. 204 205 See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance. 206 """ 207 208 self.historyFile = None 209 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 210 211 See also: `History()`. 212 """ 213 214 self.htmlHistoryFile = "index.html" 215 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 216 217 See also: `ShowHistoryChart()`. 218 """ 219 220 self.instrumentsFile = "instruments.md" 221 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 222 223 See also: `ShowInstrumentsInfo()`. 224 """ 225 226 self.searchResultsFile = "search-results.md" 227 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 228 229 See also: `SearchInstruments()`. 230 """ 231 232 self.pricesFile = "prices.md" 233 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 234 235 See also: `GetListOfPrices()`. 236 """ 237 238 self.infoFile = "info.md" 239 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 240 241 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 242 """ 243 244 self.bondsXLSXFile = "ext-bonds.xlsx" 245 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 246 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 247 248 See also: `ExtendBondsData()`. 249 """ 250 251 self.calendarFile = "calendar.md" 252 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 253 254 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 255 256 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 257 """ 258 259 self.overviewFile = "overview.md" 260 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 261 262 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 263 """ 264 265 self.overviewDigestFile = "overview-digest.md" 266 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 267 268 See also: `Overview()` with parameter `details="digest"`. 269 """ 270 271 self.overviewPositionsFile = "overview-positions.md" 272 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 273 274 See also: `Overview()` with parameter `details="positions"`. 275 """ 276 277 self.overviewOrdersFile = "overview-orders.md" 278 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 279 280 See also: `Overview()` with parameter `details="orders"`. 281 """ 282 283 self.overviewAnalyticsFile = "overview-analytics.md" 284 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 285 286 See also: `Overview()` with parameter `details="analytics"`. 287 """ 288 289 self.overviewBondsCalendarFile = "overview-calendar.md" 290 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 291 292 See also: `Overview()` with parameter `details="calendar"`. 293 """ 294 295 self.reportFile = "deals.md" 296 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 297 298 See also: `Deals()`. 299 """ 300 301 self.withdrawalLimitsFile = "limits.md" 302 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 303 304 See also: `OverviewLimits()` and `RequestLimits()`. 305 """ 306 307 self.userInfoFile = "user-info.md" 308 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 309 310 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 311 """ 312 313 self.userAccountsFile = "accounts.md" 314 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 315 316 See also: `OverviewAccounts()`, `RequestAccounts()`. 317 """ 318 319 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 320 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 321 322 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 323 324 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 325 """ 326 327 self.iList = None # init iList for raw instruments data 328 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 329 330 See also: `Listing()`, `DumpInstruments()`. 331 """ 332 333 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 334 if useCache: 335 if os.path.exists(self.iListDumpFile): 336 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 337 curTime = datetime.now(tzutc()) 338 339 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 340 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 341 342 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 343 344 else: 345 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 346 347 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 348 os.path.abspath(self.iListDumpFile), 349 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 350 )) 351 352 else: 353 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 354 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 355 356 else: 357 self.iList = self.Listing() # request new raw instruments data from broker server 358 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 359 360 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 361 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 362 363 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 364 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it!
Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}", "x-app-name": "Tim55667757.TKSBrokerAPI"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.
See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
Setter for Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: "" (empty string).
Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.
Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc.
More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.
See also: SearchByFIGI(), SearchInstruments().
446 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 447 """ 448 Send GET or POST request to broker server and receive JSON object. 449 450 self.header: must be defining with dictionary of headers. 451 self.body: if define then used as request body. None by default. 452 self.timeout: global request timeout, 15 seconds by default. 453 :param url: url with REST request. 454 :param reqType: send "GET" or "POST" request. "GET" by default. 455 :param retry: how many times retry after first request if an 5xx server errors occurred. 456 :param pause: sleep time in seconds between retries. 457 :return: response JSON (dictionary) from broker. 458 """ 459 if reqType.upper() not in ("GET", "POST"): 460 uLogger.error("You can define request type: `GET` or `POST`!") 461 raise Exception("Incorrect value") 462 463 if self.moreDebug: 464 uLogger.debug("Request parameters:") 465 uLogger.debug(" - REST API URL: {}".format(url)) 466 uLogger.debug(" - request type: {}".format(reqType)) 467 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 468 uLogger.debug(" - body:\n{}".format(self.body)) 469 470 # fast hack to avoid all operations with some tickers/FIGI 471 responseJSON = {} 472 oK = True 473 for item in self.exclude: 474 if item in url: 475 if self.moreDebug: 476 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 477 478 oK = False 479 break 480 481 if oK: 482 with self.__lock: # acquire the mutex lock 483 counter = 0 484 response = None 485 errMsg = "" 486 487 while not response and counter <= retry: 488 if reqType == "GET": 489 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 490 491 if reqType == "POST": 492 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 493 494 if self.moreDebug: 495 uLogger.debug("Response:") 496 uLogger.debug(" - status code: {}".format(response.status_code)) 497 uLogger.debug(" - reason: {}".format(response.reason)) 498 uLogger.debug(" - body length: {}".format(len(response.text))) 499 uLogger.debug(" - headers:\n{}".format(response.headers)) 500 501 # Server returns some headers: 502 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 503 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 504 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 505 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 506 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 507 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 508 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 509 sleep(rateLimitWait) 510 511 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 512 if 400 <= response.status_code < 500: 513 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 514 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 515 516 if "code" in response.text and "message" in response.text: 517 msgDict = self._ParseJSON(rawData=response.text) 518 uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"])) 519 520 counter = retry + 1 # do not retry for 4xx errors 521 522 if 500 <= response.status_code < 600: 523 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 524 uLogger.debug(" - not oK, {}".format(errMsg)) 525 526 if "code" in response.text and "message" in response.text: 527 errMsgDict = self._ParseJSON(rawData=response.text) 528 uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"])) 529 530 counter += 1 531 532 if counter <= retry: 533 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 534 sleep(pause) 535 536 responseJSON = self._ParseJSON(rawData=response.text) 537 538 if errMsg: 539 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 540 uLogger.error(" - not oK, {}".format(errMsg)) 541 542 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
575 def Listing(self) -> dict: 576 """ 577 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 578 579 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 580 """ 581 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 582 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 583 584 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 585 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 586 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 587 588 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 589 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 590 poolUpdater.close() # close the thread pool 591 poolUpdater.join() # wait a moment until all data returns from threads 592 593 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 594 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 595 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 596 597 # calculate minimum price increment (step) for all instruments and set up instrument's type: 598 for iType in iList.keys(): 599 for ticker in iList[iType]: 600 iList[iType][ticker]["type"] = iType 601 602 if "minPriceIncrement" in iList[iType][ticker].keys(): 603 iList[iType][ticker]["step"] = NanoToFloat( 604 iList[iType][ticker]["minPriceIncrement"]["units"], 605 iList[iType][ticker]["minPriceIncrement"]["nano"], 606 ) 607 608 else: 609 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 610 611 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
613 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 614 """ 615 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 616 617 See also: `DumpInstruments()`, `Listing()`. 618 619 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 620 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 621 """ 622 if self.iListDumpFile is None or not self.iListDumpFile: 623 uLogger.error("Output name of dump file must be defined!") 624 raise Exception("Filename required") 625 626 if not self.iList or forceUpdate: 627 self.iList = self.Listing() 628 629 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 630 631 # Save as XLSX with separated sheets for every type of instruments: 632 with pd.ExcelWriter( 633 path=xlsxDumpFile, 634 date_format=TKS_DATE_FORMAT, 635 datetime_format=TKS_DATE_TIME_FORMAT, 636 mode="w", 637 ) as writer: 638 for iType in TKS_INSTRUMENTS: 639 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 640 df = df[sorted(df)] # sorted by column names 641 df = df.applymap( 642 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 643 na_action="ignore", 644 ) # converting numbers from nano-type to float in every cell 645 df.to_excel( 646 writer, 647 sheet_name=iType, 648 encoding="UTF-8", 649 freeze_panes=(1, 1), 650 ) # saving as XLSX-file with freeze first row and column as headers 651 652 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
654 def DumpInstruments(self, forceUpdate: bool = True) -> str: 655 """ 656 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 657 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 658 659 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 660 661 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 662 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 663 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 664 """ 665 if self.iListDumpFile is None or not self.iListDumpFile: 666 uLogger.error("Output name of dump file must be defined!") 667 raise Exception("Filename required") 668 669 if not self.iList or forceUpdate: 670 self.iList = self.Listing() 671 672 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 673 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 674 fH.write(jsonDump) 675 676 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 677 678 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
680 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str: 681 """ 682 Show information about one instrument defined by json data and prints it in Markdown format. 683 684 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 685 686 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]` 687 :param show: if `True` then also printing information about instrument and its current price. 688 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 689 :return: multilines text in Markdown format with information about one instrument. 690 """ 691 splitLine = "| | |\n" 692 infoText = "" 693 694 if iJSON is not None and iJSON and isinstance(iJSON, dict): 695 info = [ 696 "# Main information\n\n", 697 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 698 "| Parameters | Values |\n", 699 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 700 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 701 "| Full name: | {:<54} |\n".format(iJSON["name"]), 702 ] 703 704 if "sector" in iJSON.keys() and iJSON["sector"]: 705 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 706 707 if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]: 708 info.append("| Country of instrument: | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"]))) 709 710 info.extend([ 711 splitLine, 712 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 713 "| Real exchange [Exchange section]: | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])), 714 ]) 715 716 if "isin" in iJSON.keys() and iJSON["isin"]: 717 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 718 719 if "classCode" in iJSON.keys(): 720 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 721 722 info.extend([ 723 splitLine, 724 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 725 splitLine, 726 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 727 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 728 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 729 ]) 730 731 if iJSON["figi"]: 732 self._figi = iJSON["figi"] 733 iJSON = iJSON | self.RequestTradingStatus() 734 735 info.extend([ 736 splitLine, 737 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 738 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 739 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 740 ]) 741 742 info.append(splitLine) 743 744 if "type" in iJSON.keys() and iJSON["type"]: 745 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 746 747 if "shareType" in iJSON.keys() and iJSON["shareType"]: 748 info.append("| Share type: | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]])) 749 750 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 751 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 752 753 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 754 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 755 756 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 757 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 758 759 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 760 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 761 762 if "focusType" in iJSON.keys() and iJSON["focusType"]: 763 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 764 765 if "assetType" in iJSON.keys() and iJSON["assetType"]: 766 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 767 768 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 769 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 770 771 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 772 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 773 774 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 775 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 776 777 if "currency" in iJSON.keys(): 778 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 779 780 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 781 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 782 783 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 784 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 785 786 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 787 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 788 789 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 790 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 791 792 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 793 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 794 795 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 796 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 797 798 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 799 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 800 801 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 802 info.append("| Perpetual bond: | Yes |\n") 803 804 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 805 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 806 807 iExt = None 808 if iJSON["type"] == "Bonds": 809 info.extend([ 810 splitLine, 811 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 812 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 813 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 814 iJSON["nominal"]["currency"], 815 )), 816 ]) 817 818 if "floatingCouponFlag" in iJSON.keys(): 819 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 820 821 if "amortizationFlag" in iJSON.keys(): 822 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 823 824 info.append(splitLine) 825 826 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 827 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 828 829 if iJSON["figi"]: 830 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 831 832 info.extend([ 833 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 834 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 835 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 836 ]) 837 838 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 839 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 840 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 841 iJSON["aciValue"]["currency"] 842 ))) 843 844 if "currentPrice" in iJSON.keys(): 845 info.append(splitLine) 846 847 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 848 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 849 850 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 851 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 852 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 853 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 854 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 855 856 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 857 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 858 859 info.extend([ 860 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 861 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 862 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 863 )), 864 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 865 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 866 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 867 )), 868 "| Changes between last deal price and last close | {:<54} |\n".format( 869 "{:.2f}%{}".format( 870 iJSON["currentPrice"]["changes"], 871 " ({}{:.2f} {})".format( 872 "+" if bondChangesDelta > 0 else "", 873 bondChangesDelta, 874 aciCurrency 875 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 876 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 877 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 878 currency 879 ), 880 ) 881 ), 882 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 883 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 884 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 885 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 886 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 887 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 888 )), 889 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 890 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 891 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 892 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 893 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 894 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 895 )), 896 ]) 897 898 if "lot" in iJSON.keys(): 899 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 900 901 if "step" in iJSON.keys() and iJSON["step"] != 0: 902 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 903 904 # Add bond payment calendar: 905 if iJSON["type"] == "Bonds": 906 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 907 info.extend(["\n#", strCalendar]) 908 909 infoText += "".join(info) 910 911 if show and not onlyFiles: 912 uLogger.info("{}".format(infoText)) 913 914 if self.infoFile is not None and (show or onlyFiles): 915 with open(self.infoFile, "w", encoding="UTF-8") as fH: 916 fH.write(infoText) 917 918 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 919 920 if self.useHTMLReports: 921 htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html" 922 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 923 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText)) 924 925 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 926 927 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self._ticker] - show: if
Truethen also printing information about instrument and its current price. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with information about one instrument.
929 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 930 """ 931 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 932 933 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 934 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 935 :return: JSON formatted data with information about instrument. 936 """ 937 tickerJSON = {} 938 if self.moreDebug: 939 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker)) 940 941 if not self._ticker: 942 uLogger.warning("self._ticker variable is not be empty!") 943 944 else: 945 if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 946 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker)) 947 raise Exception("Instrument not allowed") 948 949 if not self.iList: 950 self.iList = self.Listing() 951 952 if self._ticker in self.iList["Shares"].keys(): 953 tickerJSON = self.iList["Shares"][self._ticker] 954 if self.moreDebug: 955 uLogger.debug("Ticker [{}] found in shares list".format(self._ticker)) 956 957 elif self._ticker in self.iList["Currencies"].keys(): 958 tickerJSON = self.iList["Currencies"][self._ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker)) 961 962 elif self._ticker in self.iList["Bonds"].keys(): 963 tickerJSON = self.iList["Bonds"][self._ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker)) 966 967 elif self._ticker in self.iList["Etfs"].keys(): 968 tickerJSON = self.iList["Etfs"][self._ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker)) 971 972 elif self._ticker in self.iList["Futures"].keys(): 973 tickerJSON = self.iList["Futures"][self._ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in futures list".format(self._ticker)) 976 977 if tickerJSON: 978 self._figi = tickerJSON["figi"] 979 980 if requestPrice: 981 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 982 983 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 984 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 985 986 else: 987 tickerJSON["currentPrice"]["changes"] = 0 988 989 if show: 990 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 991 992 else: 993 if show: 994 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker)) 995 996 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
998 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 999 """ 1000 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1001 1002 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1003 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1004 :return: JSON formatted data with information about instrument. 1005 """ 1006 figiJSON = {} 1007 if self.moreDebug: 1008 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi)) 1009 1010 if not self._figi: 1011 uLogger.warning("self._figi variable is not be empty!") 1012 1013 else: 1014 if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1015 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi)) 1016 raise Exception("Instrument not allowed") 1017 1018 if not self.iList: 1019 self.iList = self.Listing() 1020 1021 for item in self.iList["Shares"].keys(): 1022 if self._figi == self.iList["Shares"][item]["figi"]: 1023 figiJSON = self.iList["Shares"][item] 1024 1025 if self.moreDebug: 1026 uLogger.debug("FIGI [{}] found in shares list".format(self._figi)) 1027 1028 break 1029 1030 if not figiJSON: 1031 for item in self.iList["Currencies"].keys(): 1032 if self._figi == self.iList["Currencies"][item]["figi"]: 1033 figiJSON = self.iList["Currencies"][item] 1034 1035 if self.moreDebug: 1036 uLogger.debug("FIGI [{}] found in currencies list".format(self._figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Bonds"].keys(): 1042 if self._figi == self.iList["Bonds"][item]["figi"]: 1043 figiJSON = self.iList["Bonds"][item] 1044 1045 if self.moreDebug: 1046 uLogger.debug("FIGI [{}] found in bonds list".format(self._figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Etfs"].keys(): 1052 if self._figi == self.iList["Etfs"][item]["figi"]: 1053 figiJSON = self.iList["Etfs"][item] 1054 1055 if self.moreDebug: 1056 uLogger.debug("FIGI [{}] found in etfs list".format(self._figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Futures"].keys(): 1062 if self._figi == self.iList["Futures"][item]["figi"]: 1063 figiJSON = self.iList["Futures"][item] 1064 1065 if self.moreDebug: 1066 uLogger.debug("FIGI [{}] found in futures list".format(self._figi)) 1067 1068 break 1069 1070 if figiJSON: 1071 self._figi = figiJSON["figi"] 1072 self._ticker = figiJSON["ticker"] 1073 1074 if requestPrice: 1075 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1076 1077 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1078 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1079 1080 else: 1081 figiJSON["currentPrice"]["changes"] = 0 1082 1083 if show: 1084 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1085 1086 else: 1087 if show: 1088 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi)) 1089 1090 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1092 def GetCurrentPrices(self, show: bool = True) -> dict: 1093 """ 1094 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1095 `{"buy": [{"price": 1243.8, "quantity": 193}, 1096 {"price": 1244.0, "quantity": 168}, 1097 {"price": 1244.8, "quantity": 5}, 1098 {"price": 1245.0, "quantity": 61}, 1099 {"price": 1245.4, "quantity": 60}], 1100 "sell": [{"price": 1243.6, "quantity": 8}, 1101 {"price": 1242.6, "quantity": 10}, 1102 {"price": 1242.4, "quantity": 18}, 1103 {"price": 1242.2, "quantity": 50}, 1104 {"price": 1242.0, "quantity": 113}], 1105 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1106 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1107 - sell: list of dicts with Buyers prices, 1108 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1109 - quantity: volume value by current price in lots, 1110 - limitUp: current trade session limit price, maximum, 1111 - limitDown: current trade session limit price, minimum, 1112 - lastPrice: last deal price of the instrument, 1113 - closePrice: previous trade session close price of the instrument. 1114 1115 See also: `SearchByTicker()` and `SearchByFIGI()`. 1116 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1117 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1118 1119 :param show: if `True` then print DOM to log and console. 1120 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1121 If an error occurred then returns an empty record: 1122 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1123 """ 1124 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1125 1126 if self.depth < 1: 1127 uLogger.error("Depth of Market (DOM) must be >=1!") 1128 raise Exception("Incorrect value") 1129 1130 if not (self._ticker or self._figi): 1131 uLogger.error("self._ticker or self._figi variables must be defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 if self._ticker and not self._figi: 1135 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1136 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1137 1138 if not self._ticker and self._figi: 1139 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1141 1142 if not self._figi: 1143 uLogger.error("FIGI is not defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 else: 1147 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi)) 1148 1149 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1150 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1151 self.body = str({"figi": self._figi, "depth": self.depth}) 1152 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1153 1154 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1155 # list of dicts with sellers orders: 1156 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1157 1158 # list of dicts with buyers orders: 1159 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1160 1161 # max price of instrument at this time: 1162 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1163 1164 # min price of instrument at this time: 1165 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1166 1167 # last price of deal with instrument: 1168 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1169 1170 # last close price of instrument: 1171 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1172 1173 else: 1174 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1175 uLogger.debug("Server response: {}".format(pricesResponse)) 1176 1177 if show: 1178 if prices["buy"] or prices["sell"]: 1179 info = [ 1180 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1181 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1182 self._ticker, 1183 self._figi, 1184 self.depth, 1185 ), 1186 "-" * 60, "\n", 1187 " Orders of Buyers | Orders of Sellers\n", 1188 "-" * 60, "\n", 1189 " Sell prices (volumes) | Buy prices (volumes)\n", 1190 "-" * 60, "\n", 1191 ] 1192 1193 if not prices["buy"]: 1194 info.append(" | No orders!\n") 1195 sumBuy = 0 1196 1197 else: 1198 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1199 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1200 for item in maxMinSorted: 1201 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1202 1203 if not prices["sell"]: 1204 info.append("No orders! |\n") 1205 sumSell = 0 1206 1207 else: 1208 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1209 for item in prices["sell"]: 1210 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1211 1212 info.extend([ 1213 "-" * 60, "\n", 1214 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1215 "-" * 60, "\n", 1216 ]) 1217 1218 infoText = "".join(info) 1219 1220 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1221 1222 else: 1223 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi)) 1224 1225 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1227 def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str: 1228 """ 1229 This method get and show information about all available broker instruments for current user account. 1230 If `instrumentsFile` string is not empty then also save information to this file. 1231 1232 :param show: if `True` then print results to console, if `False` — print only to file. 1233 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1234 :return: multi-lines string with all available broker instruments. 1235 """ 1236 if not self.iList: 1237 self.iList = self.Listing() 1238 1239 info = [ 1240 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1241 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1242 ] 1243 1244 # add instruments count by type: 1245 for iType in self.iList.keys(): 1246 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1247 1248 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1249 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1250 1251 # generating info tables with all instruments by type: 1252 for iType in self.iList.keys(): 1253 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1254 1255 for instrument in self.iList[iType].keys(): 1256 iName = self.iList[iType][instrument]["name"] # instrument's name 1257 if len(iName) > 57: 1258 iName = "{}...".format(iName[:54]) # right trim for a long string 1259 1260 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1261 self.iList[iType][instrument]["ticker"], 1262 iName, 1263 self.iList[iType][instrument]["figi"], 1264 self.iList[iType][instrument]["currency"], 1265 self.iList[iType][instrument]["lot"], 1266 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1267 )) 1268 1269 infoText = "".join(info) 1270 1271 if show and not onlyFiles: 1272 uLogger.info(infoText) 1273 1274 if self.instrumentsFile and (show or onlyFiles): 1275 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1276 fH.write(infoText) 1277 1278 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1279 1280 if self.useHTMLReports: 1281 htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html" 1282 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1283 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText)) 1284 1285 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1286 1287 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multi-lines string with all available broker instruments.
1289 def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict: 1290 """ 1291 This method search and show information about instruments by part of its ticker, FIGI or name. 1292 If `searchResultsFile` string is not empty then also save information to this file. 1293 1294 :param pattern: string with part of ticker, FIGI or instrument's name. 1295 :param show: if `True` then print results to console, if `False` — return list of result only. 1296 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1297 :return: list of dictionaries with all found instruments. 1298 """ 1299 if not self.iList: 1300 self.iList = self.Listing() 1301 1302 searchResults = {iType: {} for iType in self.iList} # same as iList but will contain only filtered instruments 1303 compiledPattern = re.compile(pattern, re.IGNORECASE) 1304 1305 for iType in self.iList: 1306 for instrument in self.iList[iType].values(): 1307 searchResult = compiledPattern.search(" ".join( 1308 [instrument["ticker"], instrument["figi"], instrument["name"]] 1309 )) 1310 1311 if searchResult: 1312 searchResults[iType][instrument["ticker"]] = instrument 1313 1314 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1315 info = [ 1316 "# Search results\n\n", 1317 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 1318 "* **Search pattern:** [{}]\n".format(pattern), 1319 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1320 '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n' 1321 ] 1322 infoShort = info[:] 1323 1324 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1325 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1326 skippedLine = "| ... | ... | ... | ... |\n" 1327 1328 if resultsLen == 0: 1329 info.append("\nNo results\n") 1330 infoShort.append("\nNo results\n") 1331 uLogger.warning("No results. Try changing your search pattern.") 1332 1333 else: 1334 for iType in searchResults: 1335 iTypeValuesCount = len(searchResults[iType].values()) 1336 if iTypeValuesCount > 0: 1337 info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1338 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 1340 for instrument in searchResults[iType].values(): 1341 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1342 instrument["type"], 1343 instrument["ticker"], 1344 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1345 instrument["figi"], 1346 )) 1347 1348 if iTypeValuesCount <= 5: 1349 infoShort.extend(info[-iTypeValuesCount:]) 1350 1351 else: 1352 infoShort.extend(info[-5:]) 1353 infoShort.append(skippedLine) 1354 1355 infoText = "".join(info) 1356 infoTextShort = "".join(infoShort) 1357 1358 if show and not onlyFiles: 1359 uLogger.info(infoTextShort) 1360 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1361 1362 if self.searchResultsFile and (show or onlyFiles): 1363 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1364 fH.write(infoText) 1365 1366 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1367 1368 if self.useHTMLReports: 1369 htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html" 1370 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1371 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText)) 1372 1373 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1374 1375 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of dictionaries with all found instruments.
1377 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1378 """ 1379 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1380 1381 :param instruments: list of strings with tickers or FIGIs. 1382 :return: list with unique instrument FIGIs only. 1383 """ 1384 requestedInstruments = [] 1385 for iName in instruments: 1386 if iName not in self.aliases.keys(): 1387 if iName not in requestedInstruments: 1388 requestedInstruments.append(iName) 1389 1390 else: 1391 if iName not in requestedInstruments: 1392 if self.aliases[iName] not in requestedInstruments: 1393 requestedInstruments.append(self.aliases[iName]) 1394 1395 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1396 1397 onlyUniqueFIGIs = [] 1398 for iName in requestedInstruments: 1399 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1400 continue 1401 1402 self._ticker = iName 1403 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1404 1405 if not iData: 1406 self._ticker = "" 1407 self._figi = iName 1408 1409 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1410 1411 if not iData: 1412 self._figi = "" 1413 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1414 1415 if iData and iData["figi"] not in onlyUniqueFIGIs: 1416 onlyUniqueFIGIs.append(iData["figi"]) 1417 1418 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1419 1420 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1422 def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]: 1423 """ 1424 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1425 1426 See limits: https://tinkoff.github.io/investAPI/limits/ 1427 1428 If `pricesFile` string is not empty then also save information to this file. 1429 1430 :param instruments: list of strings with tickers or FIGIs. 1431 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1432 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1433 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1434 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1435 """ 1436 if instruments is None or not instruments: 1437 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1438 raise Exception("Ticker or FIGI required") 1439 1440 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1441 1442 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1443 1444 iList = [] # trying to get info and current prices about all unique instruments: 1445 for self._figi in onlyUniqueFIGIs: 1446 iData = self.SearchByFIGI(requestPrice=True, show=False) 1447 iList.append(iData) 1448 1449 self.ShowListOfPrices(iList, show, onlyFiles) 1450 1451 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1453 def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str: 1454 """ 1455 Show table contains current prices of given instruments. 1456 1457 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1458 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1459 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1460 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1461 :return: multilines text in Markdown format as a table contains current prices. 1462 """ 1463 infoText = "" 1464 1465 if show or self.pricesFile or onlyFiles: 1466 info = [ 1467 "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1468 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1469 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1470 ] 1471 1472 for item in iList: 1473 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1474 item["ticker"], 1475 item["figi"], 1476 item["type"], 1477 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1478 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1479 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1480 "{} / {}".format( 1481 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1482 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1483 ), 1484 "{} / {}".format( 1485 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1486 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1487 ), 1488 item["currency"], 1489 )) 1490 1491 infoText = "".join(info) 1492 1493 if show and not onlyFiles: 1494 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1495 1496 if self.pricesFile and (show or onlyFiles): 1497 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1498 fH.write(infoText) 1499 1500 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1501 1502 if self.useHTMLReports: 1503 htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html" 1504 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 1505 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText)) 1506 1507 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 1508 1509 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format as a table contains current prices.
1511 def RequestTradingStatus(self) -> dict: 1512 """ 1513 Requesting trading status for the instrument defined by `figi` variable. 1514 1515 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1516 1517 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1518 1519 :return: dictionary with trading status attributes. Response example: 1520 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1521 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1522 """ 1523 if self._figi is None or not self._figi: 1524 uLogger.error("Variable `figi` must be defined for using this method!") 1525 raise Exception("FIGI required") 1526 1527 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi)) 1528 1529 self.body = str({"figi": self._figi, "instrumentId": self._figi}) 1530 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1531 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1532 1533 if self.moreDebug: 1534 uLogger.debug("Records about current trading status successfully received") 1535 1536 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1538 def RequestPortfolio(self) -> dict: 1539 """ 1540 Requesting actual user's portfolio for current `accountId`. 1541 1542 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1543 1544 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1545 1546 :return: dictionary with user's portfolio. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1556 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1557 1558 if self.moreDebug: 1559 uLogger.debug("Records about user's portfolio successfully received") 1560 1561 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1563 def RequestPositions(self) -> dict: 1564 """ 1565 Requesting open positions by currencies and instruments for current `accountId`. 1566 1567 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1568 1569 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1570 1571 :return: dictionary with open positions by instruments. 1572 """ 1573 if self.accountId is None or not self.accountId: 1574 uLogger.error("Variable `accountId` must be defined for using this method!") 1575 raise Exception("Account ID required") 1576 1577 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1578 1579 self.body = str({"accountId": self.accountId}) 1580 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1581 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1582 1583 if self.moreDebug: 1584 uLogger.debug("Records about current open positions successfully received") 1585 1586 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1588 def RequestPendingOrders(self) -> list: 1589 """ 1590 Requesting current actual pending limit orders for current `accountId`. 1591 1592 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1593 1594 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1595 1596 :return: list of dictionaries with pending limit orders. 1597 """ 1598 if self.accountId is None or not self.accountId: 1599 uLogger.error("Variable `accountId` must be defined for using this method!") 1600 raise Exception("Account ID required") 1601 1602 uLogger.debug("Requesting current actual pending limit orders. Wait, please...") 1603 1604 self.body = str({"accountId": self.accountId}) 1605 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1606 rawResponse = self.SendAPIRequest(ordersURL, reqType="POST") 1607 1608 if "orders" in rawResponse.keys(): 1609 rawOrders = rawResponse["orders"] 1610 uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders))) 1611 1612 else: 1613 rawOrders = [] 1614 uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse)) 1615 1616 return rawOrders
Requesting current actual pending limit orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending limit orders.
1618 def RequestStopOrders(self) -> list: 1619 """ 1620 Requesting current actual stop orders for current `accountId`. 1621 1622 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1623 1624 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1625 1626 :return: list of dictionaries with stop orders. 1627 """ 1628 if self.accountId is None or not self.accountId: 1629 uLogger.error("Variable `accountId` must be defined for using this method!") 1630 raise Exception("Account ID required") 1631 1632 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1633 1634 self.body = str({"accountId": self.accountId}) 1635 stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1636 rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST") 1637 1638 if "stopOrders" in rawResponse.keys(): 1639 rawStopOrders = rawResponse["stopOrders"] 1640 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1641 1642 else: 1643 rawStopOrders = [] 1644 uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse)) 1645 1646 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1648 def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict: 1649 """ 1650 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1651 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1652 and `overviewBondsCalendarFile` are defined then also save information to file. 1653 1654 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1655 many requests about the state of the portfolio, and then, based on the received data, a large number 1656 of calculation and statistics are collected. 1657 1658 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1659 :param details: how detailed should the information be? 1660 - `full` — shows full available information about portfolio status (by default), 1661 - `positions` — shows only open positions, 1662 - `orders` — shows only sections of open limits and stop orders. 1663 - `digest` — show a short digest of the portfolio status, 1664 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1665 - `calendar` — shows only the bonds calendar section (if these present in portfolio). 1666 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 1667 :return: dictionary with client's raw portfolio and some statistics. 1668 """ 1669 if self.accountId is None or not self.accountId: 1670 uLogger.error("Variable `accountId` must be defined for using this method!") 1671 raise Exception("Account ID required") 1672 1673 view = { 1674 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1675 "headers": {}, # list of dictionaries, response headers without "positions" section 1676 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1677 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1678 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1679 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1680 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1681 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1682 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1683 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1684 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1685 }, 1686 "stat": { # --- some statistics calculated using "raw" sections: 1687 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1688 "availableRUB": 0., # available rubles (without other currencies) 1689 "blockedRUB": 0., # blocked sum in Russian Rouble 1690 "totalChangesRUB": 0., # changes for all open trades in RUB 1691 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1692 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1693 "sharesCostRUB": 0., # costs of all shares in RUB 1694 "bondsCostRUB": 0., # costs of all bonds in RUB 1695 "etfsCostRUB": 0., # costs of all etfs in RUB 1696 "futuresCostRUB": 0., # costs of all futures in RUB 1697 "Currencies": [], # list of dictionaries of all currencies statistics 1698 "Shares": [], # list of dictionaries of all shares statistics 1699 "Bonds": [], # list of dictionaries of all bonds statistics 1700 "Etfs": [], # list of dictionaries of all etfs statistics 1701 "Futures": [], # list of dictionaries of all futures statistics 1702 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1703 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1704 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1705 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1706 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1707 }, 1708 "analytics": { # --- some analytics of portfolio: 1709 "distrByAssets": {}, # portfolio distribution by assets 1710 "distrByCompanies": {}, # portfolio distribution by companies 1711 "distrBySectors": {}, # portfolio distribution by sectors 1712 "distrByCurrencies": {}, # portfolio distribution by currencies 1713 "distrByCountries": {}, # portfolio distribution by countries 1714 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1715 } 1716 } 1717 1718 details = details.lower() 1719 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1720 if details not in availableDetails: 1721 details = "full" 1722 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1723 1724 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1725 1726 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1727 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1728 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending limit orders (list) 1729 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1730 1731 # save response headers without "positions" section: 1732 for key in portfolioResponse.keys(): 1733 if key != "positions": 1734 view["raw"]["headers"][key] = portfolioResponse[key] 1735 1736 else: 1737 continue 1738 1739 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1740 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1741 for item in portfolioResponse["positions"]: 1742 if item["instrumentType"] == "currency": 1743 self._figi = item["figi"] 1744 if not self._figi and item["ticker"]: 1745 self._ticker = item["ticker"] 1746 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1747 1748 curr = self.SearchByFIGI(requestPrice=False) 1749 1750 # current price of currency in RUB: 1751 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1752 "name": curr["name"], 1753 "currentPrice": NanoToFloat( 1754 item["currentPrice"]["units"], 1755 item["currentPrice"]["nano"] 1756 ), 1757 } 1758 1759 view["raw"]["Currencies"].append(item) 1760 1761 elif item["instrumentType"] == "share": 1762 view["raw"]["Shares"].append(item) 1763 1764 elif item["instrumentType"] == "bond": 1765 view["raw"]["Bonds"].append(item) 1766 1767 elif item["instrumentType"] == "etf": 1768 view["raw"]["Etfs"].append(item) 1769 1770 elif item["instrumentType"] == "futures": 1771 view["raw"]["Futures"].append(item) 1772 1773 else: 1774 continue 1775 1776 # how many volume of currencies (by ISO currency name) are blocked: 1777 for item in view["raw"]["positions"]["blocked"]: 1778 blocked = NanoToFloat(item["units"], item["nano"]) 1779 if blocked > 0: 1780 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1781 1782 # how many volume of instruments (by FIGI) are blocked: 1783 for item in view["raw"]["positions"]["securities"]: 1784 blocked = int(item["blocked"]) 1785 if blocked > 0: 1786 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1787 1788 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1789 1790 if "rub" in allBlocked.keys(): 1791 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1792 1793 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1794 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1795 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1796 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1797 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1798 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1799 view["stat"]["portfolioCostRUB"] = sum([ 1800 view["stat"]["allCurrenciesCostRUB"], 1801 view["stat"]["sharesCostRUB"], 1802 view["stat"]["bondsCostRUB"], 1803 view["stat"]["etfsCostRUB"], 1804 view["stat"]["futuresCostRUB"], 1805 ]) 1806 1807 # --- calculating some portfolio statistics: 1808 byComp = {} # distribution by companies 1809 bySect = {} # distribution by sectors 1810 byCurr = {} # distribution by currencies (include RUB) 1811 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1812 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1813 1814 for item in portfolioResponse["positions"]: 1815 self._figi = item["figi"] 1816 if not self._figi and item["ticker"]: 1817 self._ticker = item["ticker"] 1818 self._figi = self.SearchByTicker()["figi"] # Get FIGI to avoid warnings 1819 1820 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1821 1822 if instrument: 1823 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1824 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1825 1826 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1827 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1828 1829 else: 1830 blocked = 0 1831 1832 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1833 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1834 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1835 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1836 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1837 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1838 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1839 cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1840 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1841 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1842 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1843 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1844 1845 statData = { 1846 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1847 "ticker": instrument["ticker"], # ticker by FIGI 1848 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1849 "volume": volume, # available volume of instrument 1850 "lots": lots, # volume in lots of instrument 1851 "direction": direction, # direction of an instrument's position: short or long 1852 "blocked": blocked, # blocked volume of currency or instrument 1853 "currentPrice": curPrice, # current instrument's price in basic asset 1854 "average": average, # current average position price 1855 "cost": cost, # current cost of all volume of instrument in basic asset 1856 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1857 "costRUB": costRUB, # cost of instrument in ruble 1858 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1859 "profit": profit, # expected profit at current moment 1860 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1861 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1862 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1863 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1864 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1865 "step": instrument["step"], # minimum price increment 1866 } 1867 1868 # adding distribution by unique countries: 1869 if statData["country"] not in byCountry.keys(): 1870 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1871 1872 else: 1873 byCountry[statData["country"]]["cost"] += costRUB 1874 byCountry[statData["country"]]["percent"] += percentCostRUB 1875 1876 if item["instrumentType"] != "currency": 1877 # adding distribution by unique companies: 1878 if statData["name"]: 1879 if statData["name"] not in byComp.keys(): 1880 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1881 1882 else: 1883 byComp[statData["name"]]["cost"] += costRUB 1884 byComp[statData["name"]]["percent"] += percentCostRUB 1885 1886 # adding distribution by unique sectors: 1887 if statData["sector"] not in bySect.keys(): 1888 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1889 1890 else: 1891 bySect[statData["sector"]]["cost"] += costRUB 1892 bySect[statData["sector"]]["percent"] += percentCostRUB 1893 1894 # adding distribution by unique currencies: 1895 if currency not in byCurr.keys(): 1896 byCurr[currency] = { 1897 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1898 "cost": costRUB, 1899 "percent": percentCostRUB 1900 } 1901 1902 else: 1903 byCurr[currency]["cost"] += costRUB 1904 byCurr[currency]["percent"] += percentCostRUB 1905 1906 # saving statistics for every instrument: 1907 if item["instrumentType"] == "currency": 1908 view["stat"]["Currencies"].append(statData) 1909 1910 # update dict with free funds for trading (total - blocked) by currencies 1911 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1912 view["stat"]["funds"][currency] = { 1913 "total": volume, 1914 "totalCostRUB": costRUB, # total volume cost in rubles 1915 "free": volume - blocked, 1916 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1917 } 1918 1919 elif item["instrumentType"] == "share": 1920 view["stat"]["Shares"].append(statData) 1921 1922 elif item["instrumentType"] == "bond": 1923 view["stat"]["Bonds"].append(statData) 1924 1925 elif item["instrumentType"] == "etf": 1926 view["stat"]["Etfs"].append(statData) 1927 1928 elif item["instrumentType"] == "Futures": 1929 view["stat"]["Futures"].append(statData) 1930 1931 else: 1932 continue 1933 1934 # total changes in Russian Ruble: 1935 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1936 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1937 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1938 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1939 view["stat"]["funds"]["rub"] = { 1940 "total": view["stat"]["availableRUB"], 1941 "totalCostRUB": view["stat"]["availableRUB"], 1942 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1943 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1944 } 1945 1946 # --- pending limit orders sector data: 1947 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending limit orders to avoid many times price requests 1948 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1949 1950 for item in view["raw"]["orders"]: 1951 self._figi = item["figi"] 1952 1953 if item["figi"] not in uniquePendingOrdersFIGIs: 1954 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1955 1956 uniquePendingOrdersFIGIs.append(item["figi"]) 1957 uniquePendingOrders[item["figi"]] = instrument 1958 1959 else: 1960 instrument = uniquePendingOrders[item["figi"]] 1961 1962 if instrument: 1963 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1964 orderType = TKS_ORDER_TYPES[item["orderType"]] 1965 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1966 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price for order execution: 1976 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1977 1978 # necessary changes in percent to reach target from current price: 1979 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1980 1981 view["stat"]["orders"].append({ 1982 "orderID": item["orderId"], # orderId number parameter of current order 1983 "figi": item["figi"], # FIGI identification 1984 "ticker": instrument["ticker"], # ticker name by FIGI 1985 "lotsRequested": item["lotsRequested"], # requested lots value 1986 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1987 "currentPrice": lastPrice, # current instrument's price for defined action 1988 "targetPrice": target, # requested price for order execution in base currency 1989 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1990 "percentChanges": changes, # changes in percent to target from current price 1991 "currency": item["currency"], # instrument's currency name 1992 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1993 "type": orderType, # type of order from TKS_ORDER_TYPES 1994 "status": orderState, # order status from TKS_ORDER_STATES 1995 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1996 }) 1997 1998 # --- stop orders sector data: 1999 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 2000 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 2001 2002 for item in view["raw"]["stopOrders"]: 2003 self._figi = item["figi"] 2004 2005 if item["figi"] not in uniqueStopOrdersFIGIs: 2006 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 2007 2008 uniqueStopOrdersFIGIs.append(item["figi"]) 2009 uniqueStopOrders[item["figi"]] = instrument 2010 2011 else: 2012 instrument = uniqueStopOrders[item["figi"]] 2013 2014 if instrument: 2015 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 2016 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 2017 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 2018 2019 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 2020 if "expirationTime" in item.keys(): 2021 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 2022 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 2023 2024 else: 2025 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 2026 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 2027 2028 # current instrument's price (last sellers order if buy, and last buyers order if sell): 2029 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 2030 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 2031 2032 else: 2033 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 2034 2035 # requested price when stop-order executed: 2036 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2037 2038 # price for limit-order, set up when stop-order executed: 2039 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2040 2041 # necessary changes in percent to reach target from current price: 2042 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2043 2044 view["stat"]["stopOrders"].append({ 2045 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2046 "figi": item["figi"], # FIGI identification 2047 "ticker": instrument["ticker"], # ticker name by FIGI 2048 "lotsRequested": item["lotsRequested"], # requested lots value 2049 "currentPrice": lastPrice, # current instrument's price for defined action 2050 "targetPrice": target, # requested price for stop-order execution in base currency 2051 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2052 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2053 "percentChanges": changes, # changes in percent to target from current price 2054 "currency": item["currency"], # instrument's currency name 2055 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2056 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2057 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2058 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2059 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2060 }) 2061 2062 # --- calculating data for analytics section: 2063 # portfolio distribution by assets: 2064 view["analytics"]["distrByAssets"] = { 2065 "Ruble": { 2066 "uniques": 1, 2067 "cost": view["stat"]["availableRUB"], 2068 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2069 }, 2070 "Currencies": { 2071 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2072 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2073 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2074 }, 2075 "Shares": { 2076 "uniques": len(view["stat"]["Shares"]), 2077 "cost": view["stat"]["sharesCostRUB"], 2078 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2079 }, 2080 "Bonds": { 2081 "uniques": len(view["stat"]["Bonds"]), 2082 "cost": view["stat"]["bondsCostRUB"], 2083 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2084 }, 2085 "Etfs": { 2086 "uniques": len(view["stat"]["Etfs"]), 2087 "cost": view["stat"]["etfsCostRUB"], 2088 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2089 }, 2090 "Futures": { 2091 "uniques": len(view["stat"]["Futures"]), 2092 "cost": view["stat"]["futuresCostRUB"], 2093 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2094 }, 2095 } 2096 2097 # portfolio distribution by companies: 2098 view["analytics"]["distrByCompanies"]["All money cash"] = { 2099 "ticker": "", 2100 "cost": view["stat"]["allCurrenciesCostRUB"], 2101 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2102 } 2103 view["analytics"]["distrByCompanies"].update(byComp) 2104 2105 # portfolio distribution by sectors: 2106 view["analytics"]["distrBySectors"]["All money cash"] = { 2107 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2108 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2109 } 2110 view["analytics"]["distrBySectors"].update(bySect) 2111 2112 # portfolio distribution by currencies: 2113 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2114 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2115 2116 if self.moreDebug: 2117 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2118 2119 view["analytics"]["distrByCurrencies"].update(byCurr) 2120 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2121 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2122 2123 # portfolio distribution by countries: 2124 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2125 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2126 2127 if self.moreDebug: 2128 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2129 2130 view["analytics"]["distrByCountries"].update(byCountry) 2131 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2132 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2133 2134 # --- Prepare text statistics overview in human-readable: 2135 if show or onlyFiles: 2136 actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) 2137 2138 # Whatever the value `details`, header not changes: 2139 info = [ 2140 "# Client's portfolio\n\n", 2141 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2142 "* **Account ID:** [{}]\n".format(self.accountId), 2143 ] 2144 2145 if details in ["full", "positions", "digest"]: 2146 info.extend([ 2147 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2148 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2149 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2150 view["stat"]["totalChangesRUB"], 2151 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2152 view["stat"]["totalChangesPercentRUB"], 2153 ), 2154 ]) 2155 2156 if details in ["full", "positions"]: 2157 info.extend([ 2158 "## Open positions\n\n", 2159 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2160 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2161 "| **Ruble:** | {:>31} | | | | | |\n".format( 2162 "{:.2f} ({:.2f}) rub".format( 2163 view["stat"]["availableRUB"], 2164 view["stat"]["blockedRUB"], 2165 ) 2166 ) 2167 ]) 2168 2169 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2170 return [ 2171 "| | | | | | | |\n", 2172 "| {:<27} | | | | | {:>19} | |\n".format( 2173 noTradeStr if noTradeStr else typeStr, 2174 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2175 ), 2176 ] 2177 2178 def _InfoStr(data: dict, isCurr: bool = False) -> str: 2179 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2180 "{} [{}]".format(data["ticker"], data["figi"]), 2181 "{:.2f} ({:.2f}) {}".format( 2182 data["volume"], 2183 data["blocked"], 2184 data["currency"], 2185 ) if isCurr else "{:.0f} ({:.0f})".format( 2186 data["volume"], 2187 data["blocked"], 2188 ), 2189 "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."), 2190 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2191 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2192 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2193 "{}{:.2f} {} ({}{:.2f}%)".format( 2194 "+" if data["profit"] > 0 else "", 2195 data["profit"], data["baseCurrencyName"], 2196 "+" if data["percentProfit"] > 0 else "", 2197 data["percentProfit"], 2198 ), 2199 ) 2200 2201 # --- Show currencies section: 2202 if view["stat"]["Currencies"]: 2203 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2204 for item in view["stat"]["Currencies"]: 2205 info.append(_InfoStr(item, isCurr=True)) 2206 2207 else: 2208 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2209 2210 # --- Show shares section: 2211 if view["stat"]["Shares"]: 2212 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2213 2214 for item in view["stat"]["Shares"]: 2215 info.append(_InfoStr(item)) 2216 2217 else: 2218 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2219 2220 # --- Show bonds section: 2221 if view["stat"]["Bonds"]: 2222 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2223 2224 for item in view["stat"]["Bonds"]: 2225 info.append(_InfoStr(item)) 2226 2227 else: 2228 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2229 2230 # --- Show etfs section: 2231 if view["stat"]["Etfs"]: 2232 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2233 2234 for item in view["stat"]["Etfs"]: 2235 info.append(_InfoStr(item)) 2236 2237 else: 2238 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2239 2240 # --- Show futures section: 2241 if view["stat"]["Futures"]: 2242 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2243 2244 for item in view["stat"]["Futures"]: 2245 info.append(_InfoStr(item)) 2246 2247 else: 2248 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2249 2250 if details in ["full", "orders"]: 2251 # --- Show pending limit orders section: 2252 if view["stat"]["orders"]: 2253 info.extend([ 2254 "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])), 2255 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2256 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2257 ]) 2258 2259 for item in view["stat"]["orders"]: 2260 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2261 "{} [{}]".format(item["ticker"], item["figi"]), 2262 item["orderID"], 2263 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2264 "{} {} ({}{:.2f}%)".format( 2265 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2266 item["baseCurrencyName"], 2267 "+" if item["percentChanges"] > 0 else "", 2268 float(item["percentChanges"]), 2269 ), 2270 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2271 item["action"], 2272 item["type"], 2273 item["date"], 2274 )) 2275 2276 else: 2277 info.append("\n## Total pending limit-orders: [0]\n") 2278 2279 # --- Show stop orders section: 2280 if view["stat"]["stopOrders"]: 2281 info.extend([ 2282 "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])), 2283 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2284 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2285 ]) 2286 2287 for item in view["stat"]["stopOrders"]: 2288 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2289 "{} [{}]".format(item["ticker"], item["figi"]), 2290 item["orderID"], 2291 item["lotsRequested"], 2292 "{} {} ({}{:.2f}%)".format( 2293 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2294 item["baseCurrencyName"], 2295 "+" if item["percentChanges"] > 0 else "", 2296 float(item["percentChanges"]), 2297 ), 2298 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2299 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2300 item["action"], 2301 item["type"], 2302 item["expType"], 2303 item["createDate"], 2304 item["expDate"], 2305 )) 2306 2307 else: 2308 info.append("\n## Total stop-orders: [0]\n") 2309 2310 if details in ["full", "analytics"]: 2311 # -- Show analytics section: 2312 if view["stat"]["portfolioCostRUB"] > 0: 2313 info.extend([ 2314 "\n# Analytics\n\n" 2315 "* **Actual on date:** [{} UTC]\n".format(actualOnDate), 2316 "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2317 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2318 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2319 view["stat"]["totalChangesRUB"], 2320 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2321 view["stat"]["totalChangesPercentRUB"], 2322 ), 2323 "\n## Portfolio distribution by assets\n" 2324 "\n| Type | Uniques | Percent | Current cost |\n", 2325 "|------------------------------------|---------|---------|--------------------|\n", 2326 ]) 2327 2328 for key in view["analytics"]["distrByAssets"].keys(): 2329 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2330 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2331 key, 2332 view["analytics"]["distrByAssets"][key]["uniques"], 2333 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2334 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2335 )) 2336 2337 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2338 2339 info.extend([ 2340 "\n## Portfolio distribution by companies\n" 2341 "\n| Company | Percent | Current cost |\n", 2342 aSepLine, 2343 ]) 2344 2345 for company in view["analytics"]["distrByCompanies"].keys(): 2346 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2347 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2348 "{}{}".format( 2349 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2350 company, 2351 ), 2352 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2353 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2354 )) 2355 2356 info.extend([ 2357 "\n## Portfolio distribution by sectors\n" 2358 "\n| Sector | Percent | Current cost |\n", 2359 aSepLine, 2360 ]) 2361 2362 for sector in view["analytics"]["distrBySectors"].keys(): 2363 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2364 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2365 sector, 2366 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2367 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2368 )) 2369 2370 info.extend([ 2371 "\n## Portfolio distribution by currencies\n" 2372 "\n| Instruments currencies | Percent | Current cost |\n", 2373 aSepLine, 2374 ]) 2375 2376 for curr in view["analytics"]["distrByCurrencies"].keys(): 2377 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2378 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2379 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2380 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2381 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2382 )) 2383 2384 info.extend([ 2385 "\n## Portfolio distribution by countries\n" 2386 "\n| Assets by country | Percent | Current cost |\n", 2387 aSepLine, 2388 ]) 2389 2390 for country in view["analytics"]["distrByCountries"].keys(): 2391 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2392 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2393 country, 2394 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2395 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2396 )) 2397 2398 if details in ["full", "calendar"]: 2399 # -- Show bonds payment calendar section: 2400 if view["stat"]["Bonds"]: 2401 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2402 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2403 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2404 2405 else: 2406 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2407 2408 infoText = "".join(info) 2409 2410 if show and not onlyFiles: 2411 uLogger.info(infoText) 2412 2413 if details == "full" and self.overviewFile: 2414 filename = self.overviewFile 2415 2416 elif details == "digest" and self.overviewDigestFile: 2417 filename = self.overviewDigestFile 2418 2419 elif details == "positions" and self.overviewPositionsFile: 2420 filename = self.overviewPositionsFile 2421 2422 elif details == "orders" and self.overviewOrdersFile: 2423 filename = self.overviewOrdersFile 2424 2425 elif details == "analytics" and self.overviewAnalyticsFile: 2426 filename = self.overviewAnalyticsFile 2427 2428 elif details == "calendar" and self.overviewBondsCalendarFile: 2429 filename = self.overviewBondsCalendarFile 2430 2431 else: 2432 filename = "" 2433 2434 if filename and (show or onlyFiles): 2435 with open(filename, "w", encoding="UTF-8") as fH: 2436 fH.write(infoText) 2437 2438 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2439 2440 if self.useHTMLReports: 2441 htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html" 2442 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2443 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText)) 2444 2445 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2446 2447 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio).
- onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dictionary with client's raw portfolio and some statistics.
2449 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]: 2450 """ 2451 Returns history operations between two given dates for current `accountId`. 2452 If `reportFile` string is not empty then also save human-readable report. 2453 Shows some statistical data of closed positions. 2454 2455 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2456 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2457 :param show: if `True` then also prints all records to the console. 2458 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2459 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2460 :return: original list of dictionaries with history of deals records from API ("operations" key): 2461 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2462 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2463 """ 2464 if self.accountId is None or not self.accountId: 2465 uLogger.error("Variable `accountId` must be defined for using this method!") 2466 raise Exception("Account ID required") 2467 2468 startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2469 2470 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2471 2472 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2473 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2474 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2475 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2476 customStat = {} # custom statistics in additional to responseJSON 2477 2478 # --- output report in human-readable format: 2479 if self.reportFile and (show or onlyFiles): 2480 splitLine1 = "| | | | | |\n" # Summary section 2481 splitLine2 = "| | | | | | | | |\n" # Operations section 2482 nextDay = "" 2483 2484 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2485 2486 if len(ops) > 0: 2487 customStat = { 2488 "opsCount": 0, # total operations count 2489 "buyCount": 0, # buy operations 2490 "sellCount": 0, # sell operations 2491 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2492 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2493 "payIn": {"rub": 0.}, # Deposit brokerage account 2494 "payOut": {"rub": 0.}, # Withdrawals 2495 "divs": {"rub": 0.}, # Dividends income 2496 "coupons": {"rub": 0.}, # Coupon's income 2497 "brokerCom": {"rub": 0.}, # Service commissions 2498 "serviceCom": {"rub": 0.}, # Service commissions 2499 "marginCom": {"rub": 0.}, # Margin commissions 2500 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2501 } 2502 2503 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2504 for item in ops: 2505 if item["state"] == "OPERATION_STATE_EXECUTED": 2506 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2507 2508 # count buy operations: 2509 if "_BUY" in item["operationType"]: 2510 customStat["buyCount"] += 1 2511 2512 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2513 customStat["buyTotal"][item["payment"]["currency"]] += payment 2514 2515 else: 2516 customStat["buyTotal"][item["payment"]["currency"]] = payment 2517 2518 # count sell operations: 2519 elif "_SELL" in item["operationType"]: 2520 customStat["sellCount"] += 1 2521 2522 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2523 customStat["sellTotal"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["sellTotal"][item["payment"]["currency"]] = payment 2527 2528 # count incoming operations: 2529 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2530 if item["payment"]["currency"] in customStat["payIn"].keys(): 2531 customStat["payIn"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["payIn"][item["payment"]["currency"]] = payment 2535 2536 # count withdrawals operations: 2537 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2538 if item["payment"]["currency"] in customStat["payOut"].keys(): 2539 customStat["payOut"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["payOut"][item["payment"]["currency"]] = payment 2543 2544 # count dividends income: 2545 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2546 if item["payment"]["currency"] in customStat["divs"].keys(): 2547 customStat["divs"][item["payment"]["currency"]] += payment 2548 2549 else: 2550 customStat["divs"][item["payment"]["currency"]] = payment 2551 2552 # count coupon's income: 2553 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2554 if item["payment"]["currency"] in customStat["coupons"].keys(): 2555 customStat["coupons"][item["payment"]["currency"]] += payment 2556 2557 else: 2558 customStat["coupons"][item["payment"]["currency"]] = payment 2559 2560 # count broker commissions: 2561 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2562 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2563 customStat["brokerCom"][item["payment"]["currency"]] += payment 2564 2565 else: 2566 customStat["brokerCom"][item["payment"]["currency"]] = payment 2567 2568 # count service commissions: 2569 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2570 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2571 customStat["serviceCom"][item["payment"]["currency"]] += payment 2572 2573 else: 2574 customStat["serviceCom"][item["payment"]["currency"]] = payment 2575 2576 # count margin commissions: 2577 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2578 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2579 customStat["marginCom"][item["payment"]["currency"]] += payment 2580 2581 else: 2582 customStat["marginCom"][item["payment"]["currency"]] = payment 2583 2584 # count withholding taxes: 2585 elif "_TAX" in item["operationType"]: 2586 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2587 customStat["allTaxes"][item["payment"]["currency"]] += payment 2588 2589 else: 2590 customStat["allTaxes"][item["payment"]["currency"]] = payment 2591 2592 else: 2593 continue 2594 2595 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2596 2597 # --- view "Actions" lines: 2598 info.extend([ 2599 "| Report sections | | | | |\n", 2600 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2601 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2602 "| | Buy: {:<22} | {:<28} | | |\n".format( 2603 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2604 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2605 ), 2606 "| | Sell: {:<21} | {:<28} | | |\n".format( 2607 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2608 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2609 ), 2610 ]) 2611 2612 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2613 for key in opsKeys: 2614 if key == "rub": 2615 continue 2616 2617 info.extend([ 2618 "| | | {:<28} | | |\n".format( 2619 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2620 ), 2621 "| | | {:<28} | | |\n".format( 2622 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2623 ), 2624 ]) 2625 2626 info.append(splitLine1) 2627 2628 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2629 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2630 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2631 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2632 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2633 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2634 ) 2635 2636 # --- view "Payments" lines: 2637 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2638 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2639 2640 for key in paymentsKeys: 2641 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2642 2643 info.append(splitLine1) 2644 2645 # --- view "Commissions and taxes" lines: 2646 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2647 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2648 2649 for key in comKeys: 2650 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2651 2652 info.extend([ 2653 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2654 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2655 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2656 ]) 2657 2658 else: 2659 info.append("Broker returned no operations during this period\n") 2660 2661 # --- view "Operations" section: 2662 for item in ops: 2663 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2664 continue 2665 2666 else: 2667 self._figi = item["figi"] 2668 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2669 instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {} 2670 2671 # group of deals during one day: 2672 if nextDay and item["date"].split("T")[0] != nextDay: 2673 info.append(splitLine2) 2674 nextDay = "" 2675 2676 else: 2677 nextDay = item["date"].split("T")[0] # saving current day for splitting 2678 2679 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2680 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2681 self._figi if self._figi else "—", 2682 instrument["ticker"] if instrument else "—", 2683 instrument["type"] if instrument else "—", 2684 item["quantity"] if int(item["quantity"]) > 0 else "—", 2685 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2686 TKS_OPERATION_STATES[item["state"]], 2687 TKS_OPERATION_TYPES[item["operationType"]], 2688 )) 2689 2690 infoText = "".join(info) 2691 2692 if show and not onlyFiles: 2693 if self.moreDebug: 2694 uLogger.debug("Records about history of a client's operations successfully received") 2695 2696 uLogger.info(infoText) 2697 2698 if self.reportFile and (show or onlyFiles): 2699 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2700 fH.write(infoText) 2701 2702 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2703 2704 if self.useHTMLReports: 2705 htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html" 2706 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 2707 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText)) 2708 2709 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 2710 2711 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2713 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame: 2714 """ 2715 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2716 2717 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2718 Warning! Broker server used ISO UTC time by default. 2719 2720 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2721 Also, `historyFile` used to update history with `onlyMissing` parameter. 2722 2723 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2724 2725 :param start: see docstring in `TradeRoutines.GetDatesAsString()` method. 2726 :param end: see docstring in `TradeRoutines.GetDatesAsString()` method. 2727 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2728 `"hour"`, `"day"`. Default: `"hour"`. 2729 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2730 False by default. Warning! History appends only from last candle to current time 2731 with always update last candle! 2732 :param csvSep: separator if csv-file is used, `,` by default. 2733 :param show: if `True` then also prints Pandas DataFrame to the console. 2734 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 2735 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2736 `["date", "time", "open", "high", "low", "close", "volume"]`. 2737 """ 2738 strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2739 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2740 history = None # empty pandas object for history 2741 2742 if interval not in TKS_CANDLE_INTERVALS.keys(): 2743 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2744 raise Exception("Incorrect value") 2745 2746 if not (self._ticker or self._figi): 2747 uLogger.error("Ticker or FIGI must be defined!") 2748 raise Exception("Ticker or FIGI required") 2749 2750 if self._ticker and not self._figi: 2751 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2752 self._figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2753 2754 if self._figi and not self._ticker: 2755 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2756 self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2757 2758 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2759 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2760 if interval.lower() != "day": 2761 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59 2762 2763 delta = dtEnd - dtStart # current UTC time minus last time in file 2764 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2765 2766 # calculate history length in candles: 2767 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2768 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2769 length += 1 # to avoid fraction time 2770 2771 # calculate data blocks count: 2772 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2773 2774 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi)) 2775 if self.moreDebug: 2776 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2777 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2778 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2779 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2780 2781 tempOld = None # pandas object for old history, if --only-missing key present 2782 lastTime = None # datetime object of last old candle in file 2783 2784 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2785 if self.moreDebug: 2786 uLogger.debug("--only-missing key present, add only last missing candles...") 2787 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2788 2789 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2790 2791 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2792 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2793 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2794 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2795 2796 # get last datetime object from last string in file or minus 1 delta if file is empty: 2797 if len(tempOld) > 0: 2798 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2799 2800 else: 2801 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2802 2803 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2804 2805 responseJSONs = [] # raw history blocks of data 2806 2807 blockEnd = dtEnd 2808 for item in range(blocks): 2809 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2810 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2811 2812 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2813 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2814 )) 2815 2816 if blockStart == blockEnd: 2817 uLogger.debug("Skipped this zero-length block...") 2818 2819 else: 2820 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2821 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2822 self.body = str({ 2823 "figi": self._figi, 2824 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2825 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2826 "interval": TKS_CANDLE_INTERVALS[interval][0] 2827 }) 2828 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2829 2830 if "code" in responseJSON.keys(): 2831 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2832 2833 else: 2834 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2835 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2836 2837 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2838 2839 blockEnd = blockStart 2840 2841 printCount = len(responseJSONs) # candles to show in console 2842 if responseJSONs: 2843 tempHistory = pd.DataFrame( 2844 data={ 2845 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2846 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2847 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2848 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2849 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2850 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2851 "volume": [int(item["volume"]) for item in responseJSONs], 2852 }, 2853 index=range(len(responseJSONs)), 2854 columns=["date", "time", "open", "high", "low", "close", "volume"], 2855 ) 2856 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2857 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2858 2859 # append only newest candles to old history if --only-missing key present: 2860 if onlyMissing and tempOld is not None and lastTime is not None: 2861 index = 0 # find start index in tempHistory data: 2862 2863 for i, item in tempHistory.iterrows(): 2864 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2865 2866 if curTime == lastTime: 2867 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2868 index = i 2869 printCount = index + 1 2870 break 2871 2872 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2873 2874 else: 2875 history = tempHistory # if no `--only-missing` key then load full data from server 2876 2877 if self.moreDebug: 2878 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2879 2880 if history is not None and not history.empty: 2881 if show and not onlyFiles: 2882 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2883 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2884 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2885 )) 2886 2887 else: 2888 uLogger.warning("Received an empty candles history!") 2889 2890 if self.historyFile is not None: 2891 if history is not None and not history.empty: 2892 history.to_csv(self.historyFile, sep=csvSep, index=False, header=False) 2893 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile))) 2894 2895 else: 2896 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2897 2898 else: 2899 if self.moreDebug: 2900 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2901 2902 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
TradeRoutines.GetDatesAsString()method. - end: see docstring in
TradeRoutines.GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2904 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2905 """ 2906 Load candles history from csv-file and return Pandas DataFrame object. 2907 2908 See also: `History()` and `ShowHistoryChart()` methods. 2909 2910 :param filePath: path to csv-file to open. 2911 """ 2912 loadedHistory = None # init candles data object 2913 2914 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2915 2916 if os.path.exists(filePath): 2917 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2918 2919 tfStr = self.priceModel.FormattedDelta( 2920 self.priceModel.timeframe, 2921 "{days} days {hours}h {minutes}m {seconds}s", 2922 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2923 self.priceModel.timeframe, 2924 "{hours}h {minutes}m {seconds}s", 2925 ) 2926 2927 if loadedHistory is not None and not loadedHistory.empty: 2928 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2929 len(loadedHistory), 2930 tfStr, 2931 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2932 ) 2933 2934 else: 2935 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2936 2937 else: 2938 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2939 2940 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2942 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2943 """ 2944 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2945 2946 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2947 Default: `index.html` (both for interact and non-interact candlesticks chart). 2948 2949 See also: `History()` and `LoadHistory()` methods. 2950 2951 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2952 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2953 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2954 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2955 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2956 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2957 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2958 """ 2959 if isinstance(candles, str): 2960 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2961 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2962 2963 elif isinstance(candles, pd.DataFrame): 2964 self.priceModel.prices = candles # set candles chain from variable 2965 self.priceModel.ticker = self._ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2966 2967 if "datetime" not in candles.columns: 2968 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2969 2970 else: 2971 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2972 raise Exception("Incorrect value") 2973 2974 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2975 2976 if interact: 2977 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2978 2979 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2980 2981 else: 2982 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2983 2984 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2985 2986 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2988 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2989 """ 2990 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2991 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2992 2993 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2994 2995 :param operation: string "Buy" or "Sell". 2996 :param lots: volume, integer count of lots >= 1. 2997 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2998 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2999 :param expDate: string "Undefined" by default or local date in future, 3000 it is a string with format `%Y-%m-%d %H:%M:%S`. 3001 :return: JSON with response from broker server. 3002 """ 3003 if self.accountId is None or not self.accountId: 3004 uLogger.error("Variable `accountId` must be defined for using this method!") 3005 raise Exception("Account ID required") 3006 3007 if operation is None or not operation or operation not in ("Buy", "Sell"): 3008 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3009 raise Exception("Incorrect value") 3010 3011 if lots is None or lots < 1: 3012 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 3013 lots = 1 3014 3015 if tp is None or tp < 0: 3016 tp = 0 3017 3018 if sl is None or sl < 0: 3019 sl = 0 3020 3021 if expDate is None or not expDate: 3022 expDate = "Undefined" 3023 3024 if not (self._ticker or self._figi): 3025 uLogger.error("Ticker or FIGI must be defined!") 3026 raise Exception("Ticker or FIGI required") 3027 3028 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3029 self._ticker = instrument["ticker"] 3030 self._figi = instrument["figi"] 3031 3032 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate)) 3033 3034 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3035 self.body = str({ 3036 "figi": self._figi, 3037 "quantity": str(lots), 3038 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3039 "accountId": str(self.accountId), 3040 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 3041 }) 3042 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 3043 3044 if "orderId" in response.keys(): 3045 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 3046 operation, response["orderId"], 3047 self._ticker, self._figi, lots, 3048 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 3049 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 3050 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 3051 )) 3052 3053 if tp > 0: 3054 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 3055 3056 if sl > 0: 3057 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3058 3059 else: 3060 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.") 3061 3062 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3064 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3065 """ 3066 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3067 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3068 3069 See also: `Order()` and `Trade()` docstrings. 3070 3071 :param lots: volume, integer count of lots >= 1. 3072 :param tp: float > 0, take profit price of stop-order. 3073 :param sl: float > 0, stop loss price of stop-order. 3074 :param expDate: it's a local date in future. 3075 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3076 :return: JSON with response from broker server. 3077 """ 3078 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3080 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3081 """ 3082 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3083 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3084 3085 See also: `Order()` and `Trade()` docstrings. 3086 3087 :param lots: volume, integer count of lots >= 1. 3088 :param tp: float > 0, take profit price of stop-order. 3089 :param sl: float > 0, stop loss price of stop-order. 3090 :param expDate: it's a local date in the future. 3091 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3092 :return: JSON with response from broker server. 3093 """ 3094 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3096 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3097 """ 3098 Close position of given instruments. 3099 3100 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3101 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3102 This avoids unnecessary downloading data from the server. 3103 """ 3104 if instruments is None or not instruments: 3105 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3106 raise Exception("Ticker or FIGI required") 3107 3108 if isinstance(instruments, str): 3109 instruments = [instruments] 3110 3111 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3112 if uniqueInstruments: 3113 if portfolio is None or not portfolio: 3114 portfolio = self.Overview(show=False) 3115 3116 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3117 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3118 3119 for self._figi in uniqueInstruments: 3120 if self._figi not in allOpened: 3121 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi)) 3122 continue 3123 3124 # search open trade info about instrument by ticker: 3125 instrument = {} 3126 for iType in TKS_INSTRUMENTS: 3127 if instrument: 3128 break 3129 3130 for item in portfolio["stat"][iType]: 3131 if item["figi"] == self._figi: 3132 instrument = item 3133 break 3134 3135 if instrument: 3136 self._ticker = instrument["ticker"] 3137 self._figi = instrument["figi"] 3138 3139 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3140 self._ticker, 3141 self._figi, 3142 int(instrument["volume"]), 3143 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3144 )) 3145 3146 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3147 3148 if tradeLots > 0: 3149 if instrument["blocked"] > 0: 3150 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3151 instrument["blocked"], 3152 self._ticker, 3153 tradeLots, 3154 )) 3155 3156 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3157 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3158 3159 else: 3160 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3162 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3163 """ 3164 Close all positions of given instruments with defined type. 3165 3166 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3167 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3168 This avoids unnecessary downloading data from the server. 3169 """ 3170 if iType not in TKS_INSTRUMENTS: 3171 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3172 3173 else: 3174 if portfolio is None or not portfolio: 3175 portfolio = self.Overview(show=False) 3176 3177 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3178 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3179 3180 if tickers and portfolio: 3181 self.CloseTrades(tickers, portfolio) 3182 3183 else: 3184 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3186 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3187 """ 3188 Universal method to create market or limit orders with all available parameters for current `accountId`. 3189 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3190 3191 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3192 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3193 3194 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3195 then broker immediately open market order as you can do simple --buy or --sell operations! 3196 3197 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3198 When current price will go up or down to target price value then broker opens a limit order. 3199 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3200 3201 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3202 3203 :param operation: string "Buy" or "Sell". 3204 :param orderType: string "Limit" or "Stop". 3205 :param lots: volume, integer count of lots >= 1. 3206 :param targetPrice: target price > 0. This is open trade price for limit order. 3207 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3208 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3209 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3210 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3211 Stop loss order always executed by market price. 3212 :param expDate: string "Undefined" by default or local date in future. 3213 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3214 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3215 A limit order has no expiration date, it lasts until the end of the trading day. 3216 :return: JSON with response from broker server. 3217 """ 3218 if self.accountId is None or not self.accountId: 3219 uLogger.error("Variable `accountId` must be defined for using this method!") 3220 raise Exception("Account ID required") 3221 3222 if operation is None or not operation or operation not in ("Buy", "Sell"): 3223 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3224 raise Exception("Incorrect value") 3225 3226 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3227 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3228 raise Exception("Incorrect value") 3229 3230 if lots is None or lots < 1: 3231 uLogger.error("You must define trade volume > 0: integer count of lots!") 3232 raise Exception("Incorrect value") 3233 3234 if targetPrice is None or targetPrice <= 0: 3235 uLogger.error("Target price for limit-order must be greater than 0!") 3236 raise Exception("Incorrect value") 3237 3238 if limitPrice is None or limitPrice <= 0: 3239 limitPrice = targetPrice 3240 3241 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3242 stopType = "Limit" 3243 3244 if expDate is None or not expDate: 3245 expDate = "Undefined" 3246 3247 if not (self._ticker or self._figi): 3248 uLogger.error("Tocker or FIGI must be defined!") 3249 raise Exception("Ticker or FIGI required") 3250 3251 response = {} 3252 instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True) 3253 self._ticker = instrument["ticker"] 3254 self._figi = instrument["figi"] 3255 3256 if orderType == "Limit": 3257 uLogger.debug( 3258 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3259 self._ticker, self._figi, 3260 operation, lots, targetPrice, instrument["currency"], 3261 )) 3262 3263 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3264 self.body = str({ 3265 "figi": self._figi, 3266 "quantity": str(lots), 3267 "price": FloatToNano(targetPrice), 3268 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3269 "accountId": str(self.accountId), 3270 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3271 }) 3272 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3273 3274 if "orderId" in response.keys(): 3275 uLogger.info( 3276 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format( 3277 response["orderId"], self._ticker, self._figi, operation, lots, 3278 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3279 )) 3280 3281 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3282 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3283 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3284 targetPrice, instrument["currency"], 3285 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3286 )) 3287 3288 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3289 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3290 targetPrice, instrument["currency"], 3291 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3292 )) 3293 3294 else: 3295 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.") 3296 3297 if orderType == "Stop": 3298 uLogger.debug( 3299 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3300 self._ticker, self._figi, 3301 operation, lots, 3302 targetPrice, instrument["currency"], 3303 limitPrice, instrument["currency"], 3304 stopType, expDate, 3305 )) 3306 3307 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3308 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3309 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3310 3311 body = { 3312 "figi": self._figi, 3313 "quantity": str(lots), 3314 "price": FloatToNano(limitPrice), 3315 "stopPrice": FloatToNano(targetPrice), 3316 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3317 "accountId": str(self.accountId), 3318 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3319 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3320 } 3321 3322 if expDateUTC: 3323 body["expireDate"] = expDateUTC 3324 3325 self.body = str(body) 3326 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3327 3328 if "stopOrderId" in response.keys(): 3329 uLogger.info( 3330 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format( 3331 response["stopOrderId"], self._ticker, self._figi, operation, lots, 3332 "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"], 3333 "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"], 3334 TKS_STOP_ORDER_TYPES[stopOrderType], 3335 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3336 )) 3337 3338 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3339 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3340 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3341 targetPrice, instrument["currency"], 3342 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3343 )) 3344 3345 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3346 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3347 targetPrice, instrument["currency"], 3348 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3349 )) 3350 3351 else: 3352 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.") 3353 3354 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3356 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3357 """ 3358 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3359 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3360 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3361 See also: `Order()` docstring. 3362 3363 :param lots: volume, integer count of lots >= 1. 3364 :param targetPrice: target price > 0. This is open trade price for limit order. 3365 :return: JSON with response from broker server. 3366 """ 3367 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3369 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3370 """ 3371 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3372 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3373 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3374 target price value then broker opens a limit order. See also: `Order()` docstring. 3375 3376 :param lots: volume, integer count of lots >= 1. 3377 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3378 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3379 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3380 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3381 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3382 :param expDate: string "Undefined" by default or local date in future. 3383 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3384 This date is converting to UTC format for server. 3385 :return: JSON with response from broker server. 3386 """ 3387 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3389 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3390 """ 3391 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3392 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3393 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3394 See also: `Order()` docstring. 3395 3396 :param lots: volume, integer count of lots >= 1. 3397 :param targetPrice: target price > 0. This is open trade price for limit order. 3398 :return: JSON with response from broker server. 3399 """ 3400 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3402 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3403 """ 3404 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3405 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3406 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3407 target price value then broker opens a limit order. See also: `Order()` docstring. 3408 3409 :param lots: volume, integer count of lots >= 1. 3410 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3411 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3412 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3413 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3414 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3415 :param expDate: string "Undefined" by default or local date in future. 3416 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3417 This date is converting to UTC format for server. 3418 :return: JSON with response from broker server. 3419 """ 3420 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3422 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3423 """ 3424 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3425 3426 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3427 :param allOrdersIDs: pre-received lists of all active pending limit orders. 3428 This avoids unnecessary downloading data from the server. 3429 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3430 """ 3431 if self.accountId is None or not self.accountId: 3432 uLogger.error("Variable `accountId` must be defined for using this method!") 3433 raise Exception("Account ID required") 3434 3435 if orderIDs: 3436 if allOrdersIDs is None: 3437 rawOrders = self.RequestPendingOrders() 3438 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3439 3440 if allStopOrdersIDs is None: 3441 rawStopOrders = self.RequestStopOrders() 3442 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3443 3444 for orderID in orderIDs: 3445 idInPendingOrders = orderID in allOrdersIDs 3446 idInStopOrders = orderID in allStopOrdersIDs 3447 3448 if not (idInPendingOrders or idInStopOrders): 3449 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3450 continue 3451 3452 else: 3453 if idInPendingOrders: 3454 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3455 3456 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3457 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3458 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3459 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3460 3461 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3462 if self.moreDebug: 3463 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3464 3465 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3466 3467 else: 3468 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3469 3470 elif idInStopOrders: 3471 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3472 3473 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3474 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3475 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3476 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3477 3478 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3479 if self.moreDebug: 3480 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3481 3482 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3483 3484 else: 3485 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3486 3487 else: 3488 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3490 def CloseAllOrders(self) -> None: 3491 """ 3492 Gets a list of open pending and stop orders and cancel it all. 3493 """ 3494 rawOrders = self.RequestPendingOrders() 3495 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending limit orders ID 3496 lenOrders = len(allOrdersIDs) 3497 3498 rawStopOrders = self.RequestStopOrders() 3499 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3500 lenSOrders = len(allStopOrdersIDs) 3501 3502 if lenOrders > 0 or lenSOrders > 0: 3503 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3504 3505 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3506 3507 else: 3508 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3510 def CloseAll(self, *args) -> None: 3511 """ 3512 Close all available (not blocked) opened trades and orders. 3513 3514 Also, you can select one or more keywords case-insensitive: 3515 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3516 3517 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3518 """ 3519 overview = self.Overview(show=False) # get all open trades info 3520 3521 if len(args) == 0: 3522 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3523 self.CloseAllOrders() # close all pending and stop orders 3524 3525 for iType in TKS_INSTRUMENTS: 3526 if iType != "Currencies": 3527 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3528 3529 else: 3530 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3531 lowerArgs = [x.lower() for x in args] 3532 3533 if "orders" in lowerArgs: 3534 self.CloseAllOrders() # close all pending and stop orders 3535 3536 for iType in TKS_INSTRUMENTS: 3537 if iType.lower() in lowerArgs and iType != "Currencies": 3538 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3540 def CloseAllByTicker(self, instrument: str) -> None: 3541 """ 3542 Close all available (not blocked) opened trades and orders for one instrument defined by its ticker. 3543 3544 This method searches opened trade and orders of instrument throw all portfolio and then use 3545 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3546 3547 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3548 3549 :param instrument: string with ticker. 3550 """ 3551 if instrument is None or not instrument: 3552 uLogger.error("Ticker name must be defined for using this method!") 3553 raise Exception("Ticker required") 3554 3555 overview = self.Overview(show=False) # get user portfolio with all open trades info 3556 3557 self._ticker = instrument # try to set instrument as ticker 3558 self._figi = "" 3559 3560 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3561 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3562 3563 if limitAll and self.IsInLimitOrders(portfolio=overview): 3564 uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...") 3565 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3566 3567 if stopAll and self.IsInStopOrders(portfolio=overview): 3568 uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...") 3569 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3570 3571 if self.IsInPortfolio(portfolio=overview): 3572 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...") 3573 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with ticker.
3575 def CloseAllByFIGI(self, instrument: str) -> None: 3576 """ 3577 Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id. 3578 3579 This method searches opened trade and orders of instrument throw all portfolio and then use 3580 `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument. 3581 3582 See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`. 3583 3584 :param instrument: string with FIGI id. 3585 """ 3586 if instrument is None or not instrument: 3587 uLogger.error("FIGI id must be defined for using this method!") 3588 raise Exception("FIGI required") 3589 3590 overview = self.Overview(show=False) # get user portfolio with all open trades info 3591 3592 self._ticker = "" 3593 self._figi = instrument # try to set instrument as FIGI id 3594 3595 limitAll = [item["orderID"] for item in overview["stat"]["orders"]] # list of all pending limit order IDs 3596 stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]] # list of all stop order IDs 3597 3598 if limitAll and self.IsInLimitOrders(portfolio=overview): 3599 uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...") 3600 self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3601 3602 if stopAll and self.IsInStopOrders(portfolio=overview): 3603 uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...") 3604 self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll) 3605 3606 if self.IsInPortfolio(portfolio=overview): 3607 uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...") 3608 self.CloseTrades(instruments=[instrument], portfolio=overview)
Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
This method searches opened trade and orders of instrument throw all portfolio and then use
CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.
See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().
Parameters
- instrument: string with FIGI id.
3610 @staticmethod 3611 def ParseOrderParameters(operation, **inputParameters): 3612 """ 3613 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3614 3615 :param operation: string "Buy" or "Sell". 3616 :param inputParameters: this is dict of strings that looks like this 3617 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3618 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3619 "prices" key: one or more prices to open limit-orders 3620 Counts of values in lots and prices lists must be equals! 3621 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3622 """ 3623 # TODO: update order grid work with api v2 3624 pass 3625 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3626 # 3627 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3628 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3629 # raise Exception("Incorrect value") 3630 # 3631 # if "l" in inputParameters.keys(): 3632 # inputParameters["lots"] = inputParameters.pop("l") 3633 # 3634 # if "p" in inputParameters.keys(): 3635 # inputParameters["prices"] = inputParameters.pop("p") 3636 # 3637 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3638 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3639 # raise Exception("Incorrect value") 3640 # 3641 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3642 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3643 # 3644 # if len(lots) != len(prices): 3645 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3646 # raise Exception("Incorrect value") 3647 # 3648 # uLogger.debug("Extracted parameters for orders:") 3649 # uLogger.debug("lots = {}".format(lots)) 3650 # uLogger.debug("prices = {}".format(prices)) 3651 # 3652 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3653 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3654 # uLogger.debug("Order parameters: {}".format(result)) 3655 # 3656 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3658 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3659 """ 3660 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3661 3662 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3663 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3664 """ 3665 result = False 3666 msg = "Instrument not defined!" 3667 3668 if portfolio is None or not portfolio: 3669 portfolio = self.Overview(show=False) 3670 3671 if self._ticker: 3672 uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker)) 3673 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3674 3675 for iType in TKS_INSTRUMENTS: 3676 for instrument in portfolio["stat"][iType]: 3677 if instrument["ticker"] == self._ticker: 3678 result = True 3679 msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker) 3680 break 3681 3682 elif self._figi: 3683 uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi)) 3684 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3685 3686 for iType in TKS_INSTRUMENTS: 3687 for instrument in portfolio["stat"][iType]: 3688 if instrument["figi"] == self._figi: 3689 result = True 3690 msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi) 3691 break 3692 3693 else: 3694 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3695 3696 uLogger.debug(msg) 3697 3698 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3700 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3701 """ 3702 Returns instrument from the user's portfolio if it presents there. 3703 Instrument must be defined by `ticker` (highly priority) or `figi`. 3704 3705 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3706 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3707 """ 3708 result = None 3709 msg = "Instrument not defined!" 3710 3711 if portfolio is None or not portfolio: 3712 portfolio = self.Overview(show=False) 3713 3714 if self._ticker: 3715 uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker)) 3716 msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker) 3717 3718 for iType in TKS_INSTRUMENTS: 3719 for instrument in portfolio["stat"][iType]: 3720 if instrument["ticker"] == self._ticker: 3721 result = instrument 3722 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"]) 3723 break 3724 3725 elif self._figi: 3726 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi)) 3727 msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi) 3728 3729 for iType in TKS_INSTRUMENTS: 3730 for instrument in portfolio["stat"][iType]: 3731 if instrument["figi"] == self._figi: 3732 result = instrument 3733 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi) 3734 break 3735 3736 else: 3737 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3738 3739 uLogger.debug(msg) 3740 3741 return result
Returns instrument from the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3743 def IsInLimitOrders(self, portfolio: dict = None) -> bool: 3744 """ 3745 Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3746 3747 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3748 3749 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3750 :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise. 3751 """ 3752 result = False 3753 msg = "Instrument not defined!" 3754 3755 if portfolio is None or not portfolio: 3756 portfolio = self.Overview(show=False) 3757 3758 if self._ticker: 3759 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3760 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3761 3762 for instrument in portfolio["stat"]["orders"]: 3763 if instrument["ticker"] == self._ticker: 3764 result = True 3765 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3766 break 3767 3768 elif self._figi: 3769 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3770 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3771 3772 for instrument in portfolio["stat"]["orders"]: 3773 if instrument["figi"] == self._figi: 3774 result = True 3775 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3776 break 3777 3778 else: 3779 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3780 3781 uLogger.debug(msg) 3782 3783 return result
Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif limit orders list contains some limit orders for the instrument,Falseotherwise.
3785 def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]: 3786 """ 3787 Returns list with all `orderID`s of opened pending limit orders for the instrument. 3788 Instrument must be defined by `ticker` (highly priority) or `figi`. 3789 3790 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3791 3792 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3793 :return: list with `orderID`s of limit orders. 3794 """ 3795 result = [] 3796 msg = "Instrument not defined!" 3797 3798 if portfolio is None or not portfolio: 3799 portfolio = self.Overview(show=False) 3800 3801 if self._ticker: 3802 uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker)) 3803 msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker) 3804 3805 for instrument in portfolio["stat"]["orders"]: 3806 if instrument["ticker"] == self._ticker: 3807 result.append(instrument["orderID"]) 3808 3809 if result: 3810 msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker) 3811 3812 elif self._figi: 3813 uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi)) 3814 msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi) 3815 3816 for instrument in portfolio["stat"]["orders"]: 3817 if instrument["figi"] == self._figi: 3818 result.append(instrument["orderID"]) 3819 3820 if result: 3821 msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi) 3822 3823 else: 3824 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3825 3826 uLogger.debug(msg) 3827 3828 return result
Returns list with all orderIDs of opened pending limit orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of limit orders.
3830 def IsInStopOrders(self, portfolio: dict = None) -> bool: 3831 """ 3832 Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`. 3833 3834 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3835 3836 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3837 :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise. 3838 """ 3839 result = False 3840 msg = "Instrument not defined!" 3841 3842 if portfolio is None or not portfolio: 3843 portfolio = self.Overview(show=False) 3844 3845 if self._ticker: 3846 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3847 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3848 3849 for instrument in portfolio["stat"]["stopOrders"]: 3850 if instrument["ticker"] == self._ticker: 3851 result = True 3852 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3853 break 3854 3855 elif self._figi: 3856 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3857 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3858 3859 for instrument in portfolio["stat"]["stopOrders"]: 3860 if instrument["figi"] == self._figi: 3861 result = True 3862 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3863 break 3864 3865 else: 3866 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3867 3868 uLogger.debug(msg) 3869 3870 return result
Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif stop orders list contains some stop orders for the instrument,Falseotherwise.
3872 def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]: 3873 """ 3874 Returns list with all `orderID`s of opened stop orders for the instrument. 3875 Instrument must be defined by `ticker` (highly priority) or `figi`. 3876 3877 See also: `CloseAllByTicker()` and `CloseAllByFIGI()`. 3878 3879 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3880 :return: list with `orderID`s of stop orders. 3881 """ 3882 result = [] 3883 msg = "Instrument not defined!" 3884 3885 if portfolio is None or not portfolio: 3886 portfolio = self.Overview(show=False) 3887 3888 if self._ticker: 3889 uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker)) 3890 msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker) 3891 3892 for instrument in portfolio["stat"]["stopOrders"]: 3893 if instrument["ticker"] == self._ticker: 3894 result.append(instrument["orderID"]) 3895 3896 if result: 3897 msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker) 3898 3899 elif self._figi: 3900 uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi)) 3901 msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi) 3902 3903 for instrument in portfolio["stat"]["stopOrders"]: 3904 if instrument["figi"] == self._figi: 3905 result.append(instrument["orderID"]) 3906 3907 if result: 3908 msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi) 3909 3910 else: 3911 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3912 3913 uLogger.debug(msg) 3914 3915 return result
Returns list with all orderIDs of opened stop orders for the instrument.
Instrument must be defined by ticker (highly priority) or figi.
See also: CloseAllByTicker() and CloseAllByFIGI().
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
list with
orderIDs of stop orders.
3917 def RequestLimits(self) -> dict: 3918 """ 3919 Method for obtaining the available funds for withdrawal for current `accountId`. 3920 3921 See also: 3922 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3923 - `OverviewLimits()` method 3924 3925 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3926 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3927 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3928 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3929 """ 3930 if self.accountId is None or not self.accountId: 3931 uLogger.error("Variable `accountId` must be defined for using this method!") 3932 raise Exception("Account ID required") 3933 3934 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3935 3936 self.body = str({"accountId": self.accountId}) 3937 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3938 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3939 3940 if self.moreDebug: 3941 uLogger.debug("Records about available funds for withdrawal successfully received") 3942 3943 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3945 def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict: 3946 """ 3947 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3948 3949 See also: `RequestLimits()`. 3950 3951 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3952 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 3953 :return: dict with raw parsed data from server and some calculated statistics about it. 3954 """ 3955 if self.accountId is None or not self.accountId: 3956 uLogger.error("Variable `accountId` must be defined for using this method!") 3957 raise Exception("Account ID required") 3958 3959 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3960 3961 view = { 3962 "rawLimits": rawLimits, 3963 "limits": { # parsed data for every currency: 3964 "money": { # this is an array of portfolio currency positions 3965 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3966 }, 3967 "blocked": { # this is an array of blocked currency 3968 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3969 }, 3970 "blockedGuarantee": { # this is locked money under collateral for futures 3971 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3972 }, 3973 }, 3974 } 3975 3976 # --- Prepare text table with limits in human-readable format: 3977 if show or onlyFiles: 3978 info = [ 3979 "# Withdrawal limits\n\n", 3980 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3981 "* **Account ID:** [{}]\n".format(self.accountId), 3982 ] 3983 3984 if view["limits"]["money"]: 3985 info.extend([ 3986 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3987 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3988 ]) 3989 3990 else: 3991 info.append("\nNo withdrawal limits\n") 3992 3993 for curr in view["limits"]["money"].keys(): 3994 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3995 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3996 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3997 3998 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3999 "[{}]".format(curr), 4000 "{:.2f}".format(view["limits"]["money"][curr]), 4001 "{:.2f}".format(availableMoney), 4002 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 4003 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 4004 ) 4005 4006 if curr == "rub": 4007 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 4008 4009 else: 4010 info.append(infoStr) 4011 4012 infoText = "".join(info) 4013 4014 if show and not onlyFiles: 4015 uLogger.info(infoText) 4016 4017 if self.withdrawalLimitsFile and (show or onlyFiles): 4018 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 4019 fH.write(infoText) 4020 4021 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 4022 4023 if self.useHTMLReports: 4024 htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html" 4025 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4026 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText)) 4027 4028 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4029 4030 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4032 def RequestAccounts(self) -> dict: 4033 """ 4034 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 4035 4036 See also: 4037 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 4038 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 4039 - `OverviewUserInfo()` method 4040 4041 :return: dict with raw data from server that contains accounts info. Example of dict: 4042 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 4043 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 4044 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 4045 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 4046 """ 4047 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 4048 4049 self.body = str({}) 4050 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 4051 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 4052 4053 if self.moreDebug: 4054 uLogger.debug("Records about available accounts successfully received") 4055 4056 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
4058 def RequestUserInfo(self) -> dict: 4059 """ 4060 Method for requesting common user's information. 4061 4062 See also: 4063 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 4064 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 4065 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 4066 - `OverviewUserInfo()` method 4067 4068 :return: dict with raw data from server that contains user's information. Example of dict: 4069 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 4070 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 4071 """ 4072 uLogger.debug("Requesting common user's information. Wait, please...") 4073 4074 self.body = str({}) 4075 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 4076 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 4077 4078 if self.moreDebug: 4079 uLogger.debug("Records about current user successfully received") 4080 4081 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
4083 def RequestMarginStatus(self, accountId: str = None) -> dict: 4084 """ 4085 Method for requesting margin calculation for defined account ID. 4086 4087 See also: 4088 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 4089 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 4090 - `OverviewUserInfo()` method 4091 4092 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 4093 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 4094 Example of responses: 4095 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 4096 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 4097 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 4098 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 4099 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 4100 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 4101 """ 4102 if accountId is None or not accountId: 4103 if self.accountId is None or not self.accountId: 4104 uLogger.error("Variable `accountId` must be defined for using this method!") 4105 raise Exception("Account ID required") 4106 4107 else: 4108 accountId = self.accountId # use `self.accountId` (main ID) by default 4109 4110 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 4111 4112 self.body = str({"accountId": accountId}) 4113 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 4114 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 4115 4116 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 4117 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 4118 rawMargin = {} 4119 4120 else: 4121 if self.moreDebug: 4122 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 4123 4124 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
4126 def RequestTariffLimits(self) -> dict: 4127 """ 4128 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 4129 4130 See also: 4131 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 4132 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 4133 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 4134 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 4135 - `OverviewUserInfo()` method 4136 4137 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 4138 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 4139 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 4140 """ 4141 uLogger.debug("Requesting limits of current tariff. Wait, please...") 4142 4143 self.body = str({}) 4144 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 4145 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 4146 4147 if self.moreDebug: 4148 uLogger.debug("Records with limits of current tariff successfully received") 4149 4150 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
4152 def RequestBondCoupons(self, iJSON: dict) -> dict: 4153 """ 4154 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 4155 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 4156 All dates are in UTC timezone. 4157 4158 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 4159 Documentation: 4160 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 4161 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 4162 4163 See also: `ExtendBondsData()`. 4164 4165 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]` 4166 If raw iJSON is not data of bond then server returns an error [400] with message: 4167 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 4168 :return: dictionary with bond payment calendar. Response example 4169 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 4170 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 4171 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 4172 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 4173 """ 4174 if iJSON["figi"] is None or not iJSON["figi"]: 4175 uLogger.error("FIGI must be defined for using this method!") 4176 raise Exception("FIGI required") 4177 4178 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 4179 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 4180 4181 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 4182 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 4183 self._figi, 4184 startDate, 4185 endDate, 4186 )) 4187 4188 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 4189 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 4190 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 4191 4192 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 4193 uLogger.warning("Instrument type is not bond!") 4194 4195 else: 4196 if self.moreDebug: 4197 uLogger.debug("Records about bond payment calendar successfully received") 4198 4199 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self._ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
4201 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 4202 """ 4203 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 4204 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 4205 coupon yields, current yields and some statistics etc. 4206 4207 WARNING! This is too long operation if a lot of bonds requested from broker server. 4208 4209 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 4210 4211 :param instruments: list of strings with tickers or FIGIs. 4212 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 4213 for further used by data scientists or stock analytics. 4214 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 4215 In XLSX-file and Pandas DataFrame fields mean: 4216 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 4217 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 4218 """ 4219 if instruments is None or not instruments: 4220 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 4221 raise Exception("Ticker or FIGI required") 4222 4223 if isinstance(instruments, str): 4224 instruments = [instruments] 4225 4226 uniqueInstruments = self.GetUniqueFIGIs(instruments) 4227 4228 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 4229 4230 iCount = len(uniqueInstruments) 4231 tooLong = iCount >= 20 4232 if tooLong: 4233 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 4234 4235 bonds = None 4236 for i, self._figi in enumerate(uniqueInstruments): 4237 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 4238 4239 if "type" in instrument.keys() and instrument["type"] == "Bonds": 4240 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 4241 rawBond = self.SearchByFIGI(requestPrice=True) 4242 4243 # Widen raw data with UTC current time (iData["actualDateTime"]): 4244 actualDate = datetime.now(tzutc()) 4245 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 4246 4247 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 4248 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 4249 4250 # Replace some values with human-readable: 4251 iData["nominalCurrency"] = iData["nominal"]["currency"] 4252 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 4253 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 4254 iData["aciCurrency"] = iData["aciValue"]["currency"] 4255 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 4256 iData["issueSize"] = int(iData["issueSize"]) 4257 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 4258 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 4259 iData["step"] = iData["step"] if "step" in iData.keys() else 0 4260 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 4261 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 4262 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 4263 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 4264 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 4265 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 4266 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 4267 4268 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 4269 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 4270 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 4271 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 4272 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 4273 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 4274 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 4275 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 4276 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 4277 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 4278 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 4279 4280 # Widen raw data with calendar data from `rawCalendar` values: 4281 calendarData = [] 4282 if "events" in iData["rawCalendar"].keys(): 4283 for item in iData["rawCalendar"]["events"]: 4284 calendarData.append({ 4285 "couponDate": item["couponDate"], 4286 "couponNumber": int(item["couponNumber"]), 4287 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 4288 "payCurrency": item["payOneBond"]["currency"], 4289 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 4290 "couponType": TKS_COUPON_TYPES[item["couponType"]], 4291 "couponStartDate": item["couponStartDate"], 4292 "couponEndDate": item["couponEndDate"], 4293 "couponPeriod": item["couponPeriod"], 4294 }) 4295 4296 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 4297 if "maturityDate" not in iData.keys(): 4298 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 4299 4300 # Widen raw data with Coupon Rate. 4301 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 4302 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 4303 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 4304 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 4305 4306 # Widen raw data with Yield to Maturity (YTM) on current date. 4307 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4308 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4309 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4310 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4311 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4312 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4313 4314 iData["calendar"] = calendarData # adds calendar at the end 4315 4316 # Remove not used data: 4317 iData.pop("uid") 4318 iData.pop("positionUid") 4319 iData.pop("currentPrice") 4320 iData.pop("rawCalendar") 4321 4322 colNames = list(iData.keys()) 4323 if bonds is None: 4324 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4325 4326 else: 4327 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4328 4329 else: 4330 uLogger.warning("Instrument is not a bond!") 4331 4332 processed = round(100 * (i + 1) / iCount, 1) 4333 if tooLong and processed % 5 == 0: 4334 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4335 4336 else: 4337 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4338 4339 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4340 4341 # Saving bonds from Pandas DataFrame to XLSX sheet: 4342 if xlsx and self.bondsXLSXFile: 4343 with pd.ExcelWriter( 4344 path=self.bondsXLSXFile, 4345 date_format=TKS_DATE_FORMAT, 4346 datetime_format=TKS_DATE_TIME_FORMAT, 4347 mode="w", 4348 ) as writer: 4349 bonds.to_excel( 4350 writer, 4351 sheet_name="Extended bonds data", 4352 index=True, 4353 encoding="UTF-8", 4354 freeze_panes=(1, 1), 4355 ) # saving as XLSX-file with freeze first row and column as headers 4356 4357 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4358 4359 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4361 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4362 """ 4363 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4364 4365 WARNING! This is too long operation if a lot of bonds requested from broker server. 4366 4367 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4368 4369 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4370 extended information about bonds: main info, current prices, bond payment calendar, 4371 coupon yields, current yields and some statistics etc. 4372 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4373 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4374 for further used by data scientists or stock analytics. 4375 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4376 """ 4377 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4378 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False) 4379 4380 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4381 4382 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4383 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4384 calendar = None 4385 for bond in extBonds.iterrows(): 4386 for item in bond[1]["calendar"]: 4387 cData = { 4388 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4389 "couponDate": item["couponDate"], 4390 "figi": bond[1]["figi"], 4391 "ticker": bond[1]["ticker"], 4392 "name": bond[1]["name"], 4393 "couponNumber": item["couponNumber"], 4394 "payOneBond": item["payOneBond"], 4395 "payCurrency": item["payCurrency"], 4396 "couponType": item["couponType"], 4397 "couponPeriod": item["couponPeriod"], 4398 "fixDate": item["fixDate"], 4399 "couponStartDate": item["couponStartDate"], 4400 "couponEndDate": item["couponEndDate"], 4401 } 4402 4403 if calendar is None: 4404 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4405 4406 else: 4407 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4408 4409 if calendar is not None: 4410 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4411 4412 # Saving calendar from Pandas DataFrame to XLSX sheet: 4413 if xlsx: 4414 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4415 4416 with pd.ExcelWriter( 4417 path=xlsxCalendarFile, 4418 date_format=TKS_DATE_FORMAT, 4419 datetime_format=TKS_DATE_TIME_FORMAT, 4420 mode="w", 4421 ) as writer: 4422 humanReadable = calendar.copy(deep=True) 4423 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4424 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4425 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4426 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4427 humanReadable.columns = colNames # human-readable column names 4428 4429 humanReadable.to_excel( 4430 writer, 4431 sheet_name="Bond payments calendar", 4432 index=False, 4433 encoding="UTF-8", 4434 freeze_panes=(1, 2), 4435 ) # saving as XLSX-file with freeze first row and column as headers 4436 4437 del humanReadable # release df in memory 4438 4439 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4440 4441 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4443 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str: 4444 """ 4445 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4446 Also, creates Markdown file with calendar data, `calendar.md` by default. 4447 4448 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4449 4450 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4451 extended information about bonds: main info, current prices, bond payment calendar, 4452 coupon yields, current yields and some statistics etc. 4453 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4454 :param show: if `True` then also printing bonds payment calendar to the console, 4455 otherwise save to file `calendarFile` only. `False` by default. 4456 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4457 :return: multilines text in Markdown format with bonds payment calendar as a table. 4458 """ 4459 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4460 extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles) 4461 4462 infoText = "# Bond payments calendar\n\n" 4463 4464 calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles) # generate Pandas DataFrame with full calendar data 4465 4466 if not (calendar is None or calendar.empty): 4467 splitLine = "| | | | | | | | | |\n" 4468 4469 info = [ 4470 "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4471 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4472 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4473 ] 4474 4475 newMonth = False 4476 notOneBond = calendar["figi"].nunique() > 1 4477 for i, bond in enumerate(calendar.iterrows()): 4478 if newMonth and notOneBond: 4479 info.append(splitLine) 4480 4481 info.append( 4482 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4483 " √" if bond[1]["paid"] else " —", 4484 bond[1]["couponDate"].split("T")[0], 4485 bond[1]["figi"], 4486 bond[1]["ticker"], 4487 bond[1]["couponNumber"], 4488 "{} {}".format( 4489 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4490 bond[1]["payCurrency"], 4491 ), 4492 bond[1]["couponType"], 4493 bond[1]["couponPeriod"], 4494 bond[1]["fixDate"].split("T")[0], 4495 ) 4496 ) 4497 4498 if i < len(calendar.values) - 1: 4499 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4500 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4501 newMonth = False if curDate.month == nextDate.month else True 4502 4503 else: 4504 newMonth = False 4505 4506 infoText += "".join(info) 4507 4508 if show and not onlyFiles: 4509 uLogger.info("{}".format(infoText)) 4510 4511 if self.calendarFile is not None and (show or onlyFiles): 4512 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4513 fH.write(infoText) 4514 4515 uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4516 4517 if self.useHTMLReports: 4518 htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html" 4519 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4520 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText)) 4521 4522 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4523 4524 else: 4525 infoText += "No data\n" 4526 4527 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4529 def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict: 4530 """ 4531 Method for parsing and show simple table with all available user accounts. 4532 4533 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4534 4535 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4536 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4537 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4538 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4539 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4540 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4541 "closed": "—", "access": "Full access" }, ...}}` 4542 """ 4543 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4544 4545 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4546 accounts = { 4547 item["id"]: { 4548 "type": TKS_ACCOUNT_TYPES[item["type"]], 4549 "name": item["name"], 4550 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4551 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4552 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4553 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4554 } for item in rawAccounts["accounts"] 4555 } 4556 4557 # Raw and parsed data with some fields replaced in "stat" section: 4558 view = { 4559 "rawAccounts": rawAccounts, 4560 "stat": accounts, 4561 } 4562 4563 # --- Prepare simple text table with only accounts data in human-readable format: 4564 if show or onlyFiles: 4565 info = [ 4566 "# User accounts\n\n", 4567 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4568 "| Account ID | Type | Status | Name |\n", 4569 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4570 ] 4571 4572 for account in view["stat"].keys(): 4573 info.extend([ 4574 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4575 account, 4576 view["stat"][account]["type"], 4577 view["stat"][account]["status"], 4578 view["stat"][account]["name"], 4579 ) 4580 ]) 4581 4582 infoText = "".join(info) 4583 4584 if show and not onlyFiles: 4585 uLogger.info(infoText) 4586 4587 if self.userAccountsFile and (show or onlyFiles): 4588 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4589 fH.write(infoText) 4590 4591 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4592 4593 if self.useHTMLReports: 4594 htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html" 4595 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4596 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText)) 4597 4598 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4599 4600 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4602 def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict: 4603 """ 4604 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4605 4606 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4607 4608 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4609 :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files. 4610 :return: dict with raw parsed data from server and some calculated statistics about it. 4611 """ 4612 overview = self.Overview(show=False) # Request current user portfolio for the ability to calculate missing funds 4613 tmpTicker = self._ticker 4614 self._ticker = "RUB000UTSTOM" # This instrument show in rub how much money cost current margin 4615 missing = self.GetInstrumentFromPortfolio(portfolio=overview) 4616 self._ticker = tmpTicker 4617 4618 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4619 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4620 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4621 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4622 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4623 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4624 4625 # This is dict with parsed common user data: 4626 userInfo = { 4627 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4628 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4629 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4630 "tariff": rawUserInfo["tariff"], 4631 } 4632 4633 # This is an array of dict with parsed margin statuses for every account IDs: 4634 margins = {} 4635 for accountId in accounts.keys(): 4636 if rawMargins[accountId]: 4637 margins[accountId] = { 4638 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4639 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4640 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4641 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4642 "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4643 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4644 "missing": missing["volume"], 4645 } 4646 4647 else: 4648 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4649 4650 unary = {} # unary-connection limits 4651 for item in rawTariffLimits["unaryLimits"]: 4652 if item["limitPerMinute"] in unary.keys(): 4653 unary[item["limitPerMinute"]].extend(item["methods"]) 4654 4655 else: 4656 unary[item["limitPerMinute"]] = item["methods"] 4657 4658 stream = {} # stream-connection limits 4659 for item in rawTariffLimits["streamLimits"]: 4660 if item["limit"] in stream.keys(): 4661 stream[item["limit"]].extend(item["streams"]) 4662 4663 else: 4664 stream[item["limit"]] = item["streams"] 4665 4666 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4667 limits = { 4668 "unary": unary, 4669 "stream": stream, 4670 } 4671 4672 # Raw and parsed data as an output result: 4673 view = { 4674 "rawUserInfo": rawUserInfo, 4675 "rawAccounts": rawAccounts, 4676 "rawMargins": rawMargins, 4677 "rawTariffLimits": rawTariffLimits, 4678 "stat": { 4679 "overview": overview, 4680 "userInfo": userInfo, 4681 "accounts": accounts, 4682 "margins": margins, 4683 "limits": limits, 4684 }, 4685 } 4686 4687 # --- Prepare text table with user information in human-readable format: 4688 if show or onlyFiles: 4689 info = [ 4690 "# Full user information\n\n", 4691 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4692 "## Common information\n\n", 4693 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4694 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4695 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4696 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4697 "\n## User accounts\n\n", 4698 ] 4699 4700 for account in view["stat"]["accounts"].keys(): 4701 info.extend([ 4702 "### ID: [{}]\n\n".format(account), 4703 "| Parameters | Values |\n", 4704 "|----------------------|--------------------------------------------------------------|\n", 4705 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4706 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4707 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4708 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4709 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4710 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4711 ]) 4712 4713 if margins[account]: 4714 info.extend([ 4715 "| Margin status: | Enabled |\n", 4716 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4717 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4718 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4719 "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])), 4720 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4721 "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])), 4722 ]) 4723 4724 else: 4725 info.append("| Margin status: | Disabled |\n\n") 4726 4727 info.extend([ 4728 "\n## Current user tariff limits\n", 4729 "\n### See also\n", 4730 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4731 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4732 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4733 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4734 "\n### Unary limits\n", 4735 ]) 4736 4737 if unary: 4738 for key, values in sorted(unary.items()): 4739 info.append("\n* Max requests per minute: {}\n".format(key)) 4740 4741 for value in values: 4742 info.append(" - {}\n".format(value)) 4743 4744 else: 4745 info.append("\nNot available\n") 4746 4747 info.append("\n### Stream limits\n") 4748 4749 if stream: 4750 for key, values in sorted(stream.items()): 4751 info.append("\n* Max stream connections: {}\n".format(key)) 4752 4753 for value in values: 4754 info.append(" - {}\n".format(value)) 4755 4756 else: 4757 info.append("\nNot available\n") 4758 4759 infoText = "".join(info) 4760 4761 if show and not onlyFiles: 4762 uLogger.info(infoText) 4763 4764 if self.userInfoFile and (show or onlyFiles): 4765 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4766 fH.write(infoText) 4767 4768 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4769 4770 if self.useHTMLReports: 4771 htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html" 4772 with open(htmlFilePath, "w", encoding="UTF-8") as fH: 4773 fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText)) 4774 4775 uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath))) 4776 4777 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log. - onlyFiles: if
Truethen do not show Markdown table in the console, but only generates report files.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4780class Args: 4781 """ 4782 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4783 """ 4784 def __init__(self, **kwargs): 4785 self.__dict__.update(kwargs) 4786 4787 def __getattr__(self, item): 4788 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4791def ParseArgs(): 4792 """This function get and parse command line keys.""" 4793 parser = ArgumentParser() # command-line string parser 4794 4795 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4796 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4797 4798 # --- options: 4799 4800 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4801 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4802 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4803 4804 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4805 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4806 4807 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4808 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4809 4810 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4811 parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.") 4812 4813 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4814 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4815 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4816 4817 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4818 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4819 parser.add_argument("--tag", type=str, default="", help="Option: identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode. Default: `""` (empty string).") 4820 4821 # --- commands: 4822 4823 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4824 4825 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4826 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4827 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4828 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4829 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4830 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4831 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4832 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4833 4834 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4835 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4836 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4837 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4838 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4839 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4840 4841 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4842 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4843 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4844 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4845 4846 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4847 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4848 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4849 4850 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4851 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4852 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4853 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4854 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4855 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4856 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4857 4858 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4859 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4860 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4861 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4862 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.") 4863 4864 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4865 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4866 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4867 4868 cmdArgs = parser.parse_args() 4869 return cmdArgs
This function get and parse command line keys.
4872def Main(**kwargs): 4873 """ 4874 Main function for work with TKSBrokerAPI in the console. 4875 4876 See examples: 4877 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4878 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4879 """ 4880 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4881 4882 if args.debug_level: 4883 uLogger.level = 10 # always debug level by default 4884 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4885 4886 exitCode = 0 4887 start = datetime.now(tzutc()) 4888 uLogger.debug("=-" * 50) 4889 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4890 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4891 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4892 )) 4893 4894 # trying to calculate full current version: 4895 buildVersion = __version__ 4896 try: 4897 v = version("tksbrokerapi") 4898 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4899 4900 except Exception: 4901 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4902 4903 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4904 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4905 4906 try: 4907 if args.version: 4908 print("TKSBrokerAPI {}".format(buildVersion)) 4909 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4910 4911 else: 4912 # Init class for trading with Tinkoff Broker: 4913 trader = TinkoffBrokerServer( 4914 token=args.token, 4915 accountId=args.account_id, 4916 useCache=not args.no_cache, 4917 ) 4918 4919 if args.tag is not None: 4920 trader.tag = args.tag # Identification TKSBrokerAPI tag in log messages to simplify debugging when platform instances runs in parallel mode 4921 4922 # --- set some options: 4923 4924 if args.more: 4925 trader.moreDebug = True 4926 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4927 4928 if args.html: 4929 trader.useHTMLReports = True 4930 4931 if args.ticker: 4932 ticker = str(args.ticker).upper() # Tickers may be upper case only 4933 4934 if ticker in trader.aliasesKeys: 4935 trader.ticker = trader.aliases[ticker] # Replace some tickers with its aliases 4936 4937 else: 4938 trader.ticker = ticker 4939 4940 if args.figi: 4941 trader.figi = str(args.figi).upper() # FIGIs may be upper case only 4942 4943 if args.depth is not None: 4944 trader.depth = args.depth 4945 4946 # --- do one command: 4947 4948 if args.list: 4949 if args.output is not None: 4950 trader.instrumentsFile = args.output 4951 4952 trader.ShowInstrumentsInfo(show=True) 4953 4954 elif args.list_xlsx: 4955 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4956 4957 elif args.bonds_xlsx is not None: 4958 if args.output is not None: 4959 trader.bondsXLSXFile = args.output 4960 4961 if len(args.bonds_xlsx) == 0: 4962 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4963 4964 else: 4965 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4966 4967 elif args.search: 4968 if args.output is not None: 4969 trader.searchResultsFile = args.output 4970 4971 trader.SearchInstruments(pattern=args.search[0], show=True) 4972 4973 elif args.info: 4974 if not (args.ticker or args.figi): 4975 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4976 raise Exception("Ticker or FIGI required") 4977 4978 if args.output is not None: 4979 trader.infoFile = args.output 4980 4981 if args.ticker: 4982 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4983 4984 else: 4985 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4986 4987 elif args.calendar is not None: 4988 if args.output is not None: 4989 trader.calendarFile = args.output 4990 4991 if len(args.calendar) == 0: 4992 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4993 4994 else: 4995 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4996 4997 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4998 4999 elif args.price: 5000 if not (args.ticker or args.figi): 5001 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5002 raise Exception("Ticker or FIGI required") 5003 5004 trader.GetCurrentPrices(show=True) 5005 5006 elif args.prices is not None: 5007 if args.output is not None: 5008 trader.pricesFile = args.output 5009 5010 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 5011 5012 elif args.overview: 5013 if args.output is not None: 5014 trader.overviewFile = args.output 5015 5016 trader.Overview(show=True, details="full") 5017 5018 elif args.overview_digest: 5019 if args.output is not None: 5020 trader.overviewDigestFile = args.output 5021 5022 trader.Overview(show=True, details="digest") 5023 5024 elif args.overview_positions: 5025 if args.output is not None: 5026 trader.overviewPositionsFile = args.output 5027 5028 trader.Overview(show=True, details="positions") 5029 5030 elif args.overview_orders: 5031 if args.output is not None: 5032 trader.overviewOrdersFile = args.output 5033 5034 trader.Overview(show=True, details="orders") 5035 5036 elif args.overview_analytics: 5037 if args.output is not None: 5038 trader.overviewAnalyticsFile = args.output 5039 5040 trader.Overview(show=True, details="analytics") 5041 5042 elif args.overview_calendar: 5043 if args.output is not None: 5044 trader.overviewAnalyticsFile = args.output 5045 5046 trader.Overview(show=True, details="calendar") 5047 5048 elif args.deals is not None: 5049 if args.output is not None: 5050 trader.reportFile = args.output 5051 5052 if 0 <= len(args.deals) < 3: 5053 trader.Deals( 5054 start=args.deals[0] if len(args.deals) >= 1 else None, 5055 end=args.deals[1] if len(args.deals) == 2 else None, 5056 show=True, # Always show deals report in console 5057 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 5058 ) 5059 5060 else: 5061 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5062 raise Exception("Incorrect value") 5063 5064 elif args.history is not None: 5065 if args.output is not None: 5066 trader.historyFile = args.output 5067 5068 if 0 <= len(args.history) < 3: 5069 dataReceived = trader.History( 5070 start=args.history[0] if len(args.history) >= 1 else None, 5071 end=args.history[1] if len(args.history) == 2 else None, 5072 interval="hour" if args.interval is None or not args.interval else args.interval, 5073 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 5074 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 5075 show=True, # shows all downloaded candles in console 5076 ) 5077 5078 if args.render_chart is not None and dataReceived is not None: 5079 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5080 5081 trader.ShowHistoryChart( 5082 candles=dataReceived, 5083 interact=iChart, 5084 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5085 ) 5086 5087 else: 5088 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 5089 raise Exception("Incorrect value") 5090 5091 elif args.load_history is not None: 5092 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 5093 5094 if args.render_chart is not None and histData is not None: 5095 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 5096 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 5097 5098 trader.ShowHistoryChart( 5099 candles=histData, 5100 interact=iChart, 5101 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 5102 ) 5103 5104 elif args.trade is not None: 5105 if 1 <= len(args.trade) <= 5: 5106 trader.Trade( 5107 operation=args.trade[0], 5108 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 5109 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 5110 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 5111 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 5112 ) 5113 5114 else: 5115 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5116 5117 elif args.buy is not None: 5118 if 0 <= len(args.buy) <= 4: 5119 trader.Buy( 5120 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 5121 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 5122 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 5123 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 5124 ) 5125 5126 else: 5127 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5128 5129 elif args.sell is not None: 5130 if 0 <= len(args.sell) <= 4: 5131 trader.Sell( 5132 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 5133 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 5134 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 5135 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 5136 ) 5137 5138 else: 5139 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5140 5141 elif args.order: 5142 if 4 <= len(args.order) <= 7: 5143 trader.Order( 5144 operation=args.order[0], 5145 orderType=args.order[1], 5146 lots=int(args.order[2]), 5147 targetPrice=float(args.order[3]), 5148 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 5149 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 5150 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 5151 ) 5152 5153 else: 5154 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 5155 5156 elif args.buy_limit: 5157 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 5158 5159 elif args.sell_limit: 5160 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 5161 5162 elif args.buy_stop: 5163 if 2 <= len(args.buy_stop) <= 7: 5164 trader.BuyStop( 5165 lots=int(args.buy_stop[0]), 5166 targetPrice=float(args.buy_stop[1]), 5167 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 5168 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 5169 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 5170 ) 5171 5172 else: 5173 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 5174 5175 elif args.sell_stop: 5176 if 2 <= len(args.sell_stop) <= 7: 5177 trader.SellStop( 5178 lots=int(args.sell_stop[0]), 5179 targetPrice=float(args.sell_stop[1]), 5180 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 5181 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 5182 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 5183 ) 5184 5185 else: 5186 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 5187 5188 # elif args.buy_order_grid is not None: 5189 # # update order grid work with api v2 5190 # if len(args.buy_order_grid) == 2: 5191 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 5192 # 5193 # for order in orderParams: 5194 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 5195 # 5196 # else: 5197 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5198 # 5199 # elif args.sell_order_grid is not None: 5200 # # update order grid work with api v2 5201 # if len(args.sell_order_grid) >= 2: 5202 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 5203 # 5204 # for order in orderParams: 5205 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 5206 # 5207 # else: 5208 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 5209 5210 elif args.close_order is not None: 5211 trader.CloseOrders(args.close_order) # close only one order 5212 5213 elif args.close_orders is not None: 5214 trader.CloseOrders(args.close_orders) # close list of orders 5215 5216 elif args.close_trade: 5217 if not (args.ticker or args.figi): 5218 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 5219 raise Exception("Ticker or FIGI required") 5220 5221 if args.ticker: 5222 trader.CloseTrades([str(args.ticker).upper()]) # close only one trade by ticker (priority) 5223 5224 else: 5225 trader.CloseTrades([str(args.figi).upper()]) # close only one trade by FIGI 5226 5227 elif args.close_trades is not None: 5228 trader.CloseTrades(args.close_trades) # close trades for list of tickers 5229 5230 elif args.close_all is not None: 5231 if args.ticker: 5232 trader.CloseAllByTicker(instrument=str(args.ticker).upper()) 5233 5234 elif args.figi: 5235 trader.CloseAllByFIGI(instrument=str(args.figi).upper()) 5236 5237 else: 5238 trader.CloseAll(*args.close_all) 5239 5240 elif args.limits: 5241 if args.output is not None: 5242 trader.withdrawalLimitsFile = args.output 5243 5244 trader.OverviewLimits(show=True) 5245 5246 elif args.user_info: 5247 if args.output is not None: 5248 trader.userInfoFile = args.output 5249 5250 trader.OverviewUserInfo(show=True) 5251 5252 elif args.account: 5253 if args.output is not None: 5254 trader.userAccountsFile = args.output 5255 5256 trader.OverviewAccounts(show=True) 5257 5258 else: 5259 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 5260 raise Exception("There is no command to execute") 5261 5262 except Exception: 5263 trace = tb.format_exc() 5264 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 5265 if e in trace: 5266 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 5267 break 5268 5269 uLogger.debug(trace) 5270 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 5271 exitCode = 255 # an error occurred, must be open a ticket for this issue 5272 5273 finally: 5274 finish = datetime.now(tzutc()) 5275 5276 if exitCode == 0: 5277 if args.more: 5278 uLogger.debug("All operations were finished success (summary code is 0).") 5279 5280 else: 5281 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 5282 os.path.abspath(uLog.defaultLogFile), exitCode, 5283 )) 5284 5285 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 5286 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 5287 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 5288 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 5289 )) 5290 uLogger.debug("=-" * 50) 5291 5292 if not kwargs: 5293 sys.exit(exitCode) 5294 5295 else: 5296 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: